//------------------------------------------------------------------------------ // File: AtomicUniquePtr.hh // Author: Abhishek Lekshmanan - CERN //------------------------------------------------------------------------------ /************************************************************************ * EOS - the CERN Disk Storage System * * Copyright (C) 2023 CERN/Switzerland * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see .* ************************************************************************/ #pragma once #include #include namespace eos::common { /* * A thread safe unique_ptr - The main use case of this is when you have data * that is rarely changing, read often and written rarely. In this case * with a classic RWLock, even though the data is rarely changing, readers always * have to pay the cost of acquiring the lock. With this class the data load itself * is wait-free costing only a single atomic load. While the rest of the API closely * matches unique_ptr, reset() is different. We return the old value of data, * and the caller is responsible for deleting it. This is because we cannot make * any assumptions on how many readers are using the old value. So the writer has * to copy the old value and delete it after a sufficient point of synchronization. * * For really small data, you could get away with not bothering to delete and just * storing in some global list. */ template class atomic_unique_ptr { public: using pointer = T*; using element_type = T; atomic_unique_ptr() = default; atomic_unique_ptr(T* p) : p_(p) {} atomic_unique_ptr(const atomic_unique_ptr&) = delete; atomic_unique_ptr& operator=(const atomic_unique_ptr&) = delete; atomic_unique_ptr(atomic_unique_ptr&& other) { T* p = other.p_.exchange(nullptr, std::memory_order_acq_rel); publish(p); } // No move assignment operator, as to do this safely we have no idea how many // readers could be potentially accessing both the old and new values! ~atomic_unique_ptr() {; delete p_.load(std::memory_order_relaxed); } T* get() const noexcept { return p_.load(std::memory_order_acquire); } T* release() { return p_.exchange(nullptr, std::memory_order_acq_rel); } /*! * reset- the old pointer is returned instead of deleted * This is because we cannot make sure that the pointer is not being used * by another thread, so it is upto the caller to ensure a sufficient point * of synchronization where it is safe to delete the old value. When using * reset as a way to initialize the pointer, it is safe to use reset_from_null * * An atomic exchange is used over a get/publish as 2 transactions would no longer * be atomic, though calling reset from multiple threads is generally not a good * idea * @param p: pointer to be stored in the atomic_unique_ptr * @return: the old pointer * */ [[nodiscard]] T* reset(T* p) { return p_.exchange(p, std::memory_order_acq_rel); } // not TS! spinning in an atomic compare exchange can be used to make it so, // but reset_from_null is a construction routine and just like construction of // the AtomicPtr itself isn't threadsafe, this shouldn't be! void reset_from_null(T* p) { assert(p_.load(std::memory_order_acquire) == nullptr); publish(p); } T* operator->() const { return p_.load(std::memory_order_acquire); } T& operator*() const { return *this->get(); } explicit operator bool() const { return p_.load(std::memory_order_acquire) != nullptr; } private: void publish(T* p) noexcept { p_.store(p, std::memory_order_release); } std::atomic p_ {nullptr}; }; } // eos::common