【问题标题】:Atomic operations on `unique_ptr``unique_ptr` 上的原子操作
【发布时间】:2016-01-28 17:09:02
【问题描述】:

std::shared_ptr has specializations for atomic operationsatomic_compare_exchange_weak 和家人,但我找不到关于 std::unique_ptr 的等效专业化的文档。有吗?如果没有,为什么不呢?

【问题讨论】:

    标签: c++ multithreading thread-safety atomic smart-pointers


    【解决方案1】:

    可以提供std::shared_ptr 的原子实例而不能为std::unique_ptr 这样做的原因在他们的签名中有所暗示。比较:

    • std::shared_ptr<T>
    • std::unique_ptr<T, D> 其中D 是Deleter 的类型。

    std::shared_ptr 需要分配一个控制块来保存强计数和弱计数,因此删除器的类型擦除成本微不足道(只是稍微大一点的控制块)。

    因此std::shared_ptr<T>的布局大致类似于:

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

    并且可以原子地执行这两个指针的交换。


    std::unique_ptr 具有零开销策略;与使用原始指针相比,使用 std::unique_ptr 不会产生任何开销。

    因此std::unique_ptr&lt;T, D&gt;的布局大致类似于:

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

    tuple 使用 EBO(空基优化),因此只要 D 大小为零,则 sizeof(unique_ptr&lt;T&gt;) == sizeof(T*)

    但是,在 D 大小不为零的情况下,实现归结为:

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

    这个D 是这里的踢球者;一般来说,不依赖互斥体,不可能保证D 可以以原子方式交换。

    因此,无法为通用 std::unique_ptr&lt;T, D&gt; 提供一套专门的 std::atomic_compare_exchange* 例程。

    请注意,该标准甚至不保证 sizeof(unique_ptr&lt;T&gt;) == sizeof(T*) AFAIK,尽管它是一种常见的优化。

    【讨论】:

    • 嗯,它似乎是可能的,但在删除器太大的情况下,你只需要放弃 is_lock_free == true 进行实例化,像任何其他太大而无法 CAS 对象一样。
    • @BeeOnRope:比这更复杂,因为 C++ 允许自定义移动构造函数/移动赋值,所以简单的按位交换是不够的。当然,这也可以被检测到。
    【解决方案2】:

    没有std::unique_ptr 的标准原子函数。

    我确实找到了为什么不在 Herb Sutter 的 Atomic Smart Pointers(N4058) 中的论据

    Lawrence Crowl 回复添加:

    shared_ptr 锁定的原因之一是它的方式是避免我们削弱原子模板参数的前提条件,即它是微不足道的,因此没有死锁的风险。

    也就是说,我们可以削弱要求,使参数类型只需要是无锁的,或者可能只是非递归锁定。

    然而,虽然微不足道的特征可以合理地测试,但我认为没有有效的机制来测试较弱的属性。

    该提案已分配给并发子组,目前尚无处置。您可以通过JTC1/SC22/WG21 - Papers 2014mailing2014-07 查看状态

    【讨论】:

    • 该参考解释了为什么(pre c++17)std::atomic&lt;T&gt; 不适用于智能指针,但不能解释为什么没有重载例如atomic_compare_exchange_weak 用于 std::unique_ptr,而有用于 std::shared_ptr
    • 同时,N4058已修改为N4162。您可以在上面的 mailing2014-10 链接中找到它。
    【解决方案3】:

    小心,在线程之间共享可修改的unique_ptr 很少有意义,即使指针本身是原子的。如果它的内容发生了变化,其他线程怎么知道呢?他们不能。

    考虑这个例子:

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

    线程 A 在调用 p.get() 后如何避免使用悬空指针?

    如果你想在线程之间共享一个对象,请使用shared_ptr,它具有完全用于此目的的引用计数。


    如果您真的想要它,您可以随时推出自己的atomic_unique_ptr,类似的东西(简化):

    #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(); }
    };
    

    注意:本文中提供的代码特此发布到公共领域。

    【讨论】:

    • 这实际上非常有意义:它实际上是一种常见的无锁模式,用于在堆上创建一个对象,并且 - 一旦你完成它 - 将指向它的指针存储在一个共享变量中。现在,如果任何线程想要访问该对象,它首先必须以原子方式将该指针与 nullptr 交换,并在该线程完成后将指针存储回来。这样可以确保只有一个线程可以同时访问一个对象。
    • 例如,因为一个是生产者,一个是消费者(或者您甚至有多个消费者)。然后,您想将所有权转让给获得指针的任何人。无论如何:假设您有一个原子 unique_ptr,那么当您将生命周期与可见性/可访问性分开处理时,我看不出它如何简化事情。
    • 将 ::get() 分配给左值(一个比当前指令寿命更长的值)是一个坏主意,因为它会破坏使用智能指针的优势。
    • @morty 智能指针是生命周期管理的工具,但不幸的是,开发人员仍然需要在他/她的程序中推理对象生命周期。在对象的生命周期内使用原始指针仍然是一种完全有效的模式。悬挂指针甚至是正常的,只要它们在被指向对象的生命周期之外没有被取消引用。
    • @rustix:好的,重新阅读您的代码我认为您的示例让我偏离了轨道。如果我理解正确,您假设 std_unique 在两个线程之间共享(我假设 OP 做正确的事情并来回传递它)。在这种情况下,如果您执行p-&gt;do_something_long()(没有额外的原始参考),则会出现同样的问题。然而,在这种假设下, shared_ptr 本身并没有帮助。使用时需要“全局”共享指针的本地副本。注意:do_something_long()只是为了突出问题。由于它是并发的,因此方法需要多长时间并不重要。
    猜你喜欢
    • 2019-04-18
    • 2012-12-29
    • 1970-01-01
    • 1970-01-01
    • 2018-08-19
    • 2011-08-10
    • 1970-01-01
    • 2017-11-25
    • 1970-01-01
    相关资源
    最近更新 更多