考虑以下代码片段摘自Herb Sutter关于原子论的讨论:
smart_ptr类包含一个名为control_block_ptr的pimpl对象,其中包含引用计数 refs 。
// Thread A:
// smart_ptr copy ctor
smart_ptr(const smart_ptr& other) {
...
control_block_ptr = other->control_block_ptr;
control_block_ptr->refs.fetch_add(1, memory_order_relaxed);
...
}
// Thread D:
// smart_ptr destructor
~smart_ptr() {
if (control_block_ptr->refs.fetch_sub(1, memory_order_acq_rel) == 0) {
delete control_block_ptr;
}
}
Herb Sutter说,线程A中引用的增量可以使用memory_order_relaxed,因为"没有人根据动作"做任何事情。现在我理解memory_order_relaxed,如果 refs 在某个时刻等于N,并且两个线程A和B执行以下代码:
control_block_ptr->refs.fetch_add(1, memory_order_relaxed);
然后可能会发生两个线程都看到 refs 的值为N并且都将N + 1写回它。这显然不起作用,memory_order_acq_rel应该像析构函数一样使用。我哪里错了?
EDIT1:考虑以下代码。
atomic_int refs = N; // at time t0.
// [Thread 1]
refs.fetch_add(1, memory_order_relaxed); // at time t1.
// [Thread 2]
n = refs.load(memory_order_relaxed); // starting at time t2 > t1
refs.fetch_add(1, memory_order_relaxed);
n = refs.load(memory_order_relaxed);
在调用fetch_add之前,线程2观察到引用的值是多少?可能是N还是N + 1?调用fetch_add后,线程2观察到的引用值是多少?必须至少是N + 2吗?
[Talk URL:C ++&超越2012年 - http://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-2-of-2(@ 1:20:00)]
答案 0 :(得分:6)
模仿std::atomic
的Boost.Atomic库提供similar reference counting example and explanation,它可能有助于您理解。
始终可以使用
memory_order_relaxed
来增加引用计数器:对象的新引用只能从现有引用形成,并且将现有引用从一个线程传递到另一个线程必须已经提供任何所需的同步。 / p>在删除不同线程中的对象之前,必须在一个线程(通过现有引用)中强制执行对该对象的任何可能访问。这是通过删除引用之后的“释放”操作(通过此引用对对象的任何访问必须明显发生之前)和在删除对象之前的“获取”操作来实现的。
可以将
memory_order_acq_rel
用于fetch_sub操作,但是当引用计数器尚未达到零并且可能会造成性能损失时,这会导致不需要的“获取”操作。
答案 1 :(得分:2)
由于这是相当令人困惑的(至少对我来说),我将部分解决一点:
(...)然后可能会发生两个线程都认为refs的值为N并且都将N + 1写回(...)
根据this answer的@AnthonyWilliams的说法,上面的句子似乎是错误的:
唯一可以保证您拥有"最新" value是使用read-modify-write操作,如exchange(),compare_exchange_strong()或fetch_add()。读 - 修改 - 写操作有一个额外的约束条件,它们总是在"最新的"值,因此一系列线程的ai.fetch_add(1)操作序列将返回一系列没有重复或间隙的值。在没有其他限制的情况下,仍然无法保证哪些线程会看到哪些值。
因此,鉴于权威论点,我说两个线程都看不到从 N 到 N + 1 的值。
答案 2 :(得分:2)
来自std::memory_order
的C ++参考:
memory_order_relaxed:放松操作:没有同步 或者对其他读取或写入施加约束,只有这一点 操作的原子性得到保证
还有一个例子below on that page。
所以基本上,std::atomic::fetch_add()
仍然是原子的,即使使用std::memory_order_relaxed
,因此来自2个不同线程的并发refs.fetch_add(1, std::memory_order_relaxed)
总是将refs
增加2。内存顺序是如何在指定内存顺序的当前原子操作周围重新排序其他非原子或std::memory_order_relaxed
原子操作。