两个 unique_ptr<T> 的无锁交换
Posted
技术标签:
【中文标题】两个 unique_ptr<T> 的无锁交换【英文标题】:Lock-free swap of two unique_ptr<T> 【发布时间】:2013-03-17 12:43:55 【问题描述】:不能保证交换两个unique_ptr
s 是线程安全的。
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以原子方式修改两个变量的惯用方法是使用锁。
没有锁你不能为std::unique_ptr
做这件事。即使std::atomic<int>
也没有提供原子交换两个值的方法。您可以自动更新一个并取回其先前的值,但从概念上讲,交换是三个步骤,就std::atomic
API 而言,它们是:
auto tmp = a.load();
tmp = b.exchange(tmp);
a.store(tmp);
这是一个原子read,然后是一个原子read-modify-write,然后是一个原子write。每个步骤都可以原子地完成,但是如果没有锁,你不能原子地完成这三个步骤。
对于诸如std::unique_ptr<T>
之类的不可复制值,您甚至不能使用上面的load
和store
操作,但必须这样做:
auto tmp = a.exchange(nullptr);
tmp = b.exchange(tmp);
a.exchange(tmp);
这是三个 read-modify-write 操作。 (你不能真正使用 std::atomic<std::unique_ptr<T>>
来做到这一点,因为它需要一个可简单复制的参数类型,而 std::unique_ptr<T>
不是任何可复制的。)
要使用更少的操作来做到这一点,需要std::atomic
不支持的不同 API,因为它无法实现,因为正如 Stas 的回答所说,大多数处理器都不可能。 C++ 标准没有将功能标准化的习惯,这在所有当代架构上都是不可能的。 (反正不是故意的!)
编辑:您更新的问题询问了一个非常不同的问题,在第二个示例中,您不需要影响两个对象的原子交换。只有global
在线程之间共享,所以您不必关心local
的更新是否是原子的,您只需要自动更新global
并检索旧值。规范的 C++11 方法是使用 std:atomic<T*>
,您甚至不需要第二个变量:
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->update(...)
时,音频线程上没有任何东西可以让 t
保持活动状态
【讨论】:
您的构造函数效率低下,并在未初始化的垃圾上使用delete
。使用初始化列表将 arg 传递给 atomic<T*>
构造函数,例如 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<x>
891:
struct __atomic_base // false
mutable _Atomic(_Tp) __a_; // <-- here?
1103:
template <class _Tp>
struct atomic<_Tp*>
struct atomic<_Tp*>
: public __atomic_base<_Tp*>
typedef __atomic_base<_Tp*> __base;
4@4341typedef __atomic_base<_Tp*> __base;
987654341typedef __atomic_base<_Tp*> __base;
987654341@987654341
添加了推荐的解决方案@PeterCordes 感谢您的建议!!
现在您已经为atomic<T*>
构造函数添加了nullptr
默认参数是安全的,因此您在构造它时只是在无用地删除nullptr
,并执行了不必要的原子RMW。安全但缓慢。以前你让它默认构造,只有在静态存储中才会安全。在自动或动态存储中,atomicTptr
可能会在构造函数中的 update()
之前持有垃圾。以上是关于两个 unique_ptr<T> 的无锁交换的主要内容,如果未能解决你的问题,请参考以下文章
将所有权从 unique_ptr<T,void(*)(T*)> 转移到 unique_ptr<const T,void(*)(const T*)>
`unique_ptr< T const [] >` 是不是应该接受 `T*` 构造函数参数?