两个 unique_ptr<T> 的无锁交换

Posted

技术标签:

【中文标题】两个 unique_ptr<T> 的无锁交换【英文标题】:Lock-free swap of two unique_ptr<T> 【发布时间】:2013-03-17 12:43:55 【问题描述】:

不能保证交换两个unique_ptrs 是线程安全的。

std::unique_ptr<T> a, b;
std::swap(a, b); // not threadsafe

由于我需要原子指针交换并且我喜欢 unique_ptr 的所有权处理,有没有一种简单的方法可以将它们结合起来?


编辑:如果这是不可能的,我愿意接受替代方案。我至少想做这样的事情:

threadshared_unique_ptr<T> global;

void f() 
   threadlocal_unique_ptr<T> local(new T(...));
   local.swap_content(global); // atomically for global

在 C++11 中这样做的惯用方式是什么?

【问题讨论】:

首先,您将如何处理T* @JonathanWakely:老实说,我不知道。这就是我问这个问题的原因。我已经稍微放松了我的问题,T* 现在应该可以了。 有一个建议在isocpp.org/blog/2014/06/n4058 添加 atomic (和 atomic )现在对您没有帮助,但评论可能有用。 (而且我认为通过与 atomic 一起工作的原始 T* 指针是目前你能做的最好的事情)。 【参考方案1】:

以原子方式修改两个变量的惯用方法是使用锁。

没有锁你不能为std::unique_ptr 做这件事。即使std::atomic&lt;int&gt; 也没有提供原子交换两个值的方法。您可以自动更新一个并取回其先前的值,但从概念上讲,交换是三个步骤,就std::atomic API 而言,它们是:

auto tmp = a.load();
tmp = b.exchange(tmp);
a.store(tmp);

这是一个原子read,然后是一个原子read-modify-write,然后是一个原子write。每个步骤都可以原子地完成,但是如果没有锁,你不能原子地完成这三个步骤。

对于诸如std::unique_ptr&lt;T&gt; 之类的不可复制值,您甚至不能使用上面的loadstore 操作,但必须这样做:

auto tmp = a.exchange(nullptr);
tmp = b.exchange(tmp);
a.exchange(tmp);

这是三个 read-modify-write 操作。 (你不能真正使用 std::atomic&lt;std::unique_ptr&lt;T&gt;&gt; 来做到这一点,因为它需要一个可简单复制的参数类型,而 std::unique_ptr&lt;T&gt; 不是任何可复制的。)

要使用更少的操作来做到这一点,需要std::atomic 不支持的不同 API,因为它无法实现,因为正如 Stas 的回答所说,大多数处理器都不可能。 C++ 标准没有将功能标准化的习惯,这在所有当代架构上都是不可能的。 (反正不是故意的!)

编辑:您更新的问题询问了一个非常不同的问题,在第二个示例中,您不需要影响两个对象的原子交换。只有global 在线程之间共享,所以您不必关心local 的更新是否是原子的,您只需要自动更新global 并检索旧值。规范的 C++11 方法是使用 std:atomic&lt;T*&gt;,您甚至不需要第二个变量:

atomic<T*> global;

void f() 
   delete global.exchange(new T(...));

这是一个单一的read-modify-write操作。

【讨论】:

是的,你完全正确! CAS 在那里不需要。原子交换绰绰​​有余。看来我对原来的问题太着迷了,CAS 看起来已经很简单了)如果你不介意,我会把这个包含在我的答案中。【参考方案2】:

两个指针的无锁交换

似乎没有针对此问题的通用无锁解决方案。为此,您需要能够将新值原子地写入两个非连续的内存位置。这称为DCAS,但在英特尔处理器中不可用。

所有权的无锁转移

这是可能的,因为它只需要原子地将新值保存到global 并接收它的旧值。我的第一个想法是使用CAS 操作。看看下面的代码就知道了:

std::atomic<T*> global;

void f() 
   T* local = new T;
   T* temp = nullptr;
   do 
       temp = global;                                                   // 1
    while(!std::atomic_compare_exchange_weak(&global, &temp, local));  // 2

   delete temp;

步骤

    记住当前global指针在temp 如果global 仍然等于temp(它没有被其他线程更改),则将local 保存到global。如果不是这样,请重试。

实际上,CAS 在那里有点矫枉过正,因为在旧的global 值被改变之前,我们没有做任何特别的事情。所以,我们就可以使用原子交换操作:

std::atomic<T*> global;

void f() 
   T* local = new T;
   T* temp = std::atomic_exchange(&global, local);
   delete temp;

请参阅 Jonathan 的 answer 以获得更简短和优雅的解决方案。

无论如何,您都必须编写自己的智能指针。您不能将这个技巧与标准 unique_ptr 一起使用。

【讨论】:

您将为std::atomic_exchange 操作设置什么类型的内存同步顺序?默认的memory_order_seq_cst 是否可以,或者可能是矫枉过正?【参考方案3】:

这是一个有效的解决方案

您必须编写自己的智能指针

template<typename T>
struct SmartAtomicPtr

    SmartAtomicPtr( T* newT )
    
        update( newT );
    
    ~SmartAtomicPtr()
    
        update(nullptr);
    
    void update( T* newT, std::memory_order ord = memory_order_seq_cst ) 
    
        delete atomicTptr.exchange( newT, ord );
    
    std::shared_ptr<T> get(std::memory_order ord = memory_order_seq_cst) 
     
        keepAlive.reset( atomicTptr.load(ord) );
        return keepAlive;
    
private:
    std::atomic<T*> atomicTptrnullptr;
    std::shared_ptr<T> keepAlive;
;

它基于@Jonathan Wakely 最后的 sn-p。

希望这样的事情是安全的:

/*audio thread*/ auto t = ptr->get() ); 
/*GUI thread*/ ptr->update( new T() );
/*audio thread*/ t->doSomething(); 

问题是你可以这样做:

/*audio thread*/ auto* t = ptr->get(); 
/*GUI thread*/ ptr->update( new T() );
/*audio thread*/ t->doSomething(); 

当 GUI 线程调用 ptr-&gt;update(...) 时,音频线程上没有任何东西可以让 t 保持活动状态

【讨论】:

您的构造函数效率低下,并在未初始化的垃圾上使用delete。使用初始化列表将 arg 传递给 atomic&lt;T*&gt; 构造函数,例如 SmartAtomicPtr( T* newT ) : atomicTptr(newT) 。您的析构函数也不需要执行原子 RMW,只需 get(),除非您希望多个线程可以安全地销毁对象... 另外,您可能想要使用std::memory_order ord = memory_order_seq_cst 参数,以便您可以轻松更新或获取,但默认排序仍然是 seq-cst。 为了我自己的理解(我对这个原子的东西很陌生),我在 Xcode 中挖掘了 std::atomic ,试图找到你所说的垃圾值来自哪里。 607: #define _Atomic(x) __gcc_atomic::__gcc_atomic_t&lt;x&gt;891:struct __atomic_base // falsemutable _Atomic(_Tp) __a_; // &lt;-- here?1103:template &lt;class _Tp&gt;struct atomic&lt;_Tp*&gt;struct atomic&lt;_Tp*&gt;: public __atomic_base&lt;_Tp*&gt;typedef __atomic_base&lt;_Tp*&gt; __base;4@4341typedef __atomic_base&lt;_Tp*&gt; __base;987654341typedef __atomic_base&lt;_Tp*&gt; __base;987654341@987654341 添加了推荐的解决方案@PeterCordes 感谢您的建议!! 现在您已经为atomic&lt;T*&gt; 构造函数添加了nullptr 默认参数是安全的,因此您在构造它时只是在无用地删除nullptr,并执行了不必要的原子RMW。安全但缓慢。以前你让它默认构造,只有在静态存储中才会安全。在自动或动态存储中,atomicTptr 可能会在构造函数中的 update() 之前持有垃圾。

以上是关于两个 unique_ptr<T> 的无锁交换的主要内容,如果未能解决你的问题,请参考以下文章

为啥 unique_ptr<T> 不能从 T* 构造?

将所有权从 unique_ptr<T,void(*)(T*)> 转移到 unique_ptr<const T,void(*)(const T*)>

`unique_ptr< T const [] >` 是不是应该接受 `T*` 构造函数参数?

vector<unique_ptr<T> > 作为基类成员

来自 T* 的 std::unique_ptr<T> 的构造函数显式背后的原因是啥?

std::list< std::unique_ptr<T> >:传递它