Atomic operations on `unique_ptr`

后端 未结 3 905
既然无缘
既然无缘 2021-01-08 01:04

std::shared_ptr has specializations for atomic operations like atomic_compare_exchange_weak and family, but I cannot find documentation on equivale

相关标签:
3条回答
  • 2021-01-08 01:12

    No there no standard atomic functions for std::unique_ptr.

    I did find an argument for why not in Atomic Smart Pointers(N4058) by Herb Sutter

    Lawrence Crowl responded to add:

    One of the reasons that shared_ptr locking is the way it is is to avoid a situation in which we weaken the precondition on the atomic template parameter that it be trivial, and hence have no risk of deadlock.

    That said, we could weaken the requirement so that the argument type only needs to be lockfree, or perhaps only non-recursively locking.

    However, while trivial makes for reasonably testable traits, I see no effective mechanism to test for the weaker property.

    That proposal has been assigned to the Concurrency Subgroup and has no disposition as of yet. You can check the status at JTC1/SC22/WG21 - Papers 2014 mailing2014-07

    0 讨论(0)
  • 2021-01-08 01:24

    Be careful, sharing a modifiable unique_ptr between threads rarely makes sense, even if the pointer itself was atomic. If its contents changes, how can other threads know about it? They can't.

    Consider this example:

    unique_ptr<MyObject> p(new MyObject);
    
    // Thread A
    auto ptr = p.get();
    if (ptr) {
        ptr->do_something();
    }
    
    // Thread B
    p.reset();
    

    How can Thread A avoid using a dangling pointer after calling p.get()?

    If you want to share an object between threads, use shared_ptr which has reference counting exactly for this purpose.


    If you really wanted it, you can always roll your own atomic_unique_ptr, something along the lines (simplified):

    #pragma once
    #include <atomic>
    #include <memory>
    
    template<class T>
    class atomic_unique_ptr
    {
      using pointer = T *;
      std::atomic<pointer> ptr;
    public:
      constexpr atomic_unique_ptr() noexcept : ptr() {}
      explicit atomic_unique_ptr(pointer p) noexcept : ptr(p) {}
      atomic_unique_ptr(atomic_unique_ptr&& p) noexcept : ptr(p.release()) {}
      atomic_unique_ptr& operator=(atomic_unique_ptr&& p) noexcept { reset(p.release()); return *this; }
      atomic_unique_ptr(std::unique_ptr<T>&& p) noexcept : ptr(p.release()) {}
      atomic_unique_ptr& operator=(std::unique_ptr<T>&& p) noexcept { reset(p.release()); return *this; }
    
      void reset(pointer p = pointer()) { auto old = ptr.exchange(p); if (old) delete old; }
      operator pointer() const { return ptr; }
      pointer operator->() const { return ptr; }
      pointer get() const { return ptr; }
      explicit operator bool() const { return ptr != pointer(); }
      pointer release() { return ptr.exchange(pointer()); }
      ~atomic_unique_ptr() { reset(); }
    };
    
    template<class T>
    class atomic_unique_ptr<T[]> // for array types
    {
      using pointer = T *;
      std::atomic<pointer> ptr;
    public:
      constexpr atomic_unique_ptr() noexcept : ptr() {}
      explicit atomic_unique_ptr(pointer p) noexcept : ptr(p) {}
      atomic_unique_ptr(atomic_unique_ptr&& p) noexcept : ptr(p.release()) {}
      atomic_unique_ptr& operator=(atomic_unique_ptr&& p) noexcept { reset(p.release()); return *this; }
      atomic_unique_ptr(std::unique_ptr<T>&& p) noexcept : ptr(p.release()) {}
      atomic_unique_ptr& operator=(std::unique_ptr<T>&& p) noexcept { reset(p.release()); return *this; }
    
      void reset(pointer p = pointer()) { auto old = ptr.exchange(p); if (old) delete[] old; }
      operator pointer() const { return ptr; }
      pointer operator->() const { return ptr; }
      pointer get() const { return ptr; }
      explicit operator bool() const { return ptr != pointer(); }
      pointer release() { return ptr.exchange(pointer()); }
      ~atomic_unique_ptr() { reset(); }
    };
    

    NB: The code provided in this post is hereby released into Public Domain.

    0 讨论(0)
  • 2021-01-08 01:29

    The reason that it is possible to provide an atomic instance of std::shared_ptr and it is not possible to do so for std::unique_ptr is hinted at in their signature. Compare:

    • std::shared_ptr<T> vs
    • std::unique_ptr<T, D> where D is the type of the Deleter.

    std::shared_ptr needs to allocate a control-block where the strong and weak count are kept, so type-erasure of the deleter came at a trivial cost (a simply slightly larger control-block).

    As a result, the layout of std::shared_ptr<T> is generally similar to:

    template <typename T>
    struct shared_ptr {
        T* _M_ptr;
        SomeCounterClass<T>* _M_counters;
    };
    

    And it is possible to atomically perform the exchange of those two pointers.


    std::unique_ptr has a zero-overhead policy; using a std::unique_ptr should not incur any overhead compared to using a raw pointer.

    As a result, the layout of std::unique_ptr<T, D> is generally similar to:

    template <typename T, typename D = default_delete<T>>
    struct unique_ptr {
        tuple<T*, D> _M_t;
    };
    

    Where the tuple uses EBO (Empty Base Optimization) so that whenever D is zero-sized then sizeof(unique_ptr<T>) == sizeof(T*).

    However, in the cases where D is NOT zero-sized, the implementation boils down to:

    template <typename T, typename D = default_delete<T>>
    struct unique_ptr {
        T* _M_ptr;
        D _M_del;
    };
    

    This D is the kicker here; it is not possible, in general, to guarantee that D can be exchange in an atomic fashion without relying on mutexes.

    Therefore, it is not possible to provide an std::atomic_compare_exchange* suite of specialized routine for the generic std::unique_ptr<T, D>.

    Note that the standard does not even guarantee that sizeof(unique_ptr<T>) == sizeof(T*) AFAIK, though it's a common optimization.

    0 讨论(0)
提交回复
热议问题