说,我在线程#1中创建了一个Foo
类型的对象,并希望能够在线程#3中访问它。
我可以尝试类似的东西:
std::atomic<int> sync{10};
Foo *fp;
// thread 1: modifies sync: 10 -> 11
fp = new Foo;
sync.store(11, std::memory_order_release);
// thread 2a: modifies sync: 11 -> 12
while (sync.load(std::memory_order_relaxed) != 11);
sync.store(12, std::memory_order_relaxed);
// thread 3
while (sync.load(std::memory_order_acquire) != 12);
fp->do_something();
Foo
,更新为11 sync
的值递增为12 该场景被破坏,因为线程#3旋转直到它加载12,其可能无序到达(wrt 11)并且Foo
没有与12一起订购(由于线程#2a中的放松操作)。
这有点违反直觉,因为sync
的修改顺序是10→11→12
标准说(§1.10.1-6):
原子存储 - 释放与从存储中获取其值的load-acquire同步(29.3)。 [注意:除了在指定的情况下,读取更高的值不一定能确保可见性,如下所述。这样的要求有时会干扰有效的实施。 - 后注]
它也在(§1.10.1-5)中说:
由原子对象M上的释放操作A引导的释放序列是M的修改顺序中的副作用的最大连续子序列,其中第一操作是A,并且每个后续操作
- 由执行A或
的相同线程执行 - 是一个原子读 - 修改 - 写操作。
现在,修改了线程#2a以使用原子读 - 修改 - 写操作:
// thread 2b: modifies sync: 11 -> 12
int val;
while ((val = 11) && !sync.compare_exchange_weak(val, 12, std::memory_order_relaxed));
如果此版本序列正确,则Foo
在加载11或12时与线程#3同步。
关于使用原子读 - 修改 - 写的问题是:
如果是这样的话:
答案 0 :(得分:2)
线程#2b的场景是否构成了正确的发布序列?
是,根据标准引用。
确保此方案正确的读取 - 修改 - 写入操作的特定属性是什么?
嗯,有点循环的答案是唯一重要的特定属性是“C ++标准如此定义”。
实际上,人们可能会问为什么标准会像这样定义它。我不认为你会发现答案有深刻的理论基础:我认为委员会也可以定义它,使得RMW 不参与释放序列,或者更难以定义,以便两者 RMW和单独的mo_relaxed
加载和存储参与发布序列,而不会影响模型的“健全性”。
他们已经提供了与他们为什么不选择后一种方法有关的表现:
这样的要求有时会影响有效的实施。
特别是,在允许加载存储重新排序的任何硬件平台上,这意味着即使mo_relaxed
加载和/或存储也可能需要障碍!这样的平台今天存在。即使在更有序的平台上,它也可能会禁止编译器优化。
那么他们为什么不采取其他“一致”方法,不要求RMW mo_relaxed
参与释放序列?可能是因为RMW操作的现有硬件实现提供了这样的保证,并且RMW操作的性质使得将来可能会这样。特别是,正如Peter在上面的评论中指出的那样,RMW操作,即使mo_relaxed
在概念上和实际上 1 比单独的加载和存储更强:如果他们不这样做,它们将毫无用处总订单一致。
一旦你接受硬件是如何工作的,从性能角度来看是否合理标准:如果你没有,你会让人们使用更严格的排序,例如mo_acq_rel
来获得释放顺序保证,但在具有弱排序CAS的真实硬件上,这不是免费的。
1 “实际”部分意味着即使最弱形式的RMW指令通常也是相对“昂贵”的操作,在现代硬件上需要十几个周期或更多,而mo_relaxed
加载和商店通常只是编译到目标ISA中的普通加载和存储。