gloo/common/memory.h (58 lines of code) (raw):

/** * Copyright (c) 2019-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ #pragma once #ifndef _WIN32 #include <unistd.h> #endif #include <memory> #include <thread> namespace gloo { // The lifecycle of the unbound buffer is controlled by the user. // It may be constructed as value or unique_ptr and destructed when // going out of scope or when unwinding the stack. // // For example: // // { // int i = 0; // auto buf = context->createUnboundBuffer(&i, sizeof(i)); // buf->send(1, 0); // ... // throw std::runtime_error("Something happened!"); // ... // buf->waitSend(); // } // // // At this point, if `waitSend` was not yet called, the I/O // // thread executing the send operation may still call into it. // // The unbound buffer will have been destructed already, // // so we need to make sure the reference gets invalidated. // // If upon destruction there are pending operations (e.g. a recv // operation timed out), there will be references to this unbound // buffer in the `transport::Pair` instances that could fulfill the // operation. To avoid these pairs calling into the unbound buffer // after it is destructed, we must ensure that these references are // invalidated before the destructor returns. Also, if destruction of // the unbound buffer races with the recv operation completing after // all, we must block the destructor and wait for the operation to // finish. Otherwise, we risk the device thread writing to memory it's // not supposed to. // // We solve this by using a shared_ptr of the "this" pointer of the // unbound buffer. This doesn't magically convert the unbound buffer // to be a shared_ptr, but it allows for handing out weak_ptr // instances to refer to it. Then, whenever the unbound buffer is // used by another thread, it converts the weak_ptr into a shared_ptr // and uses it for a very short period of time. The wrapper class below // waits for all shared_ptr instances to be released before returning // from its destructor. This will block indefinitely if the shared_ptr // acquired from the weak_ptr stays alive. // // Forward definitions. template <typename T> class WeakNonOwningPtr; template <typename T> class ShareableNonOwningPtr; // NonOwningPtr is constructed from a WeakNonOwningPtr, if and // only if the underlying object is still alive. If it is, destruction // of the underlying object is blocked until the NonOwningPtr // is destructed. It boxes a shared_ptr instead of being typedef'd as // one to prevent misuse (e.g. extending its lifetime). template <typename T> class NonOwningPtr final { public: NonOwningPtr() {} explicit NonOwningPtr(const WeakNonOwningPtr<T>& ptr) : ptr_(ptr.ptr_.lock()) {} T* operator->() const noexcept { return ptr_.get(); } explicit operator bool() const noexcept { return (bool)ptr_; } private: std::shared_ptr<T> ptr_; }; // WeakNonOwningPtr can be constructed from a ShareableNonOwningPtr. // It can instantiate a NonOwningPtr if and only if the // underlying object is still alive. It boxes a weak_ptr instead of // being typedef'd as one because it must instantiate the // NonOwningPtr type instead of a raw shared_ptr. template <typename T> class WeakNonOwningPtr final { public: WeakNonOwningPtr() {} explicit WeakNonOwningPtr(const ShareableNonOwningPtr<T>& ref) : ptr_(ref.ptr_) {} // Returns true if the instance was initialized. explicit operator bool() const noexcept { // Per std::weak_ptr::owner_before, "[...] two smart pointers // compare equivalent only if they are both empty or if they both // own the same object [...]". Therefore, if owner_before is true // in either direction w.r.t. an empty weak_ptr, the instance was // initialized. return ptr_.owner_before(std::weak_ptr<T>{}) || std::weak_ptr<T>{}.owner_before(ptr_); } protected: std::weak_ptr<T> ptr_; friend class NonOwningPtr<T>; }; // ShareableNonOwningPtr is the root reference to the this pointer. It // can instantiate WeakNonOwningPtr instances. The destructor blocks // until all NonOwningPtr instances have been destroyed. This // guarantees that the object it refers to has no other active // references (it may have weak ones) and can safely be destructed. template <typename T> class ShareableNonOwningPtr final { public: // Initializes private shared_ptr with nop deallocation function. explicit ShareableNonOwningPtr(T* t) : ptr_(t, [](void* ptr) {}) {} // Disable copy constructors. ShareableNonOwningPtr(const ShareableNonOwningPtr&) = delete; ShareableNonOwningPtr& operator=(ShareableNonOwningPtr const&) = delete; ~ShareableNonOwningPtr() { // Acquire weak_ptr to T auto weak = std::weak_ptr<T>(ptr_); // Release shared_ptr to T ptr_.reset(); // Wait for all shared_ptr's to T have been released while (!weak.expired()) { std::this_thread::yield(); } } protected: std::shared_ptr<T> ptr_; friend class WeakNonOwningPtr<T>; }; } // namespace gloo