#include <iostream>
#include <atomic>
#include <thread>
#include <cassert>
int main(){
std::atomic<int> v{0};
int x = 0;
std::thread t1([&](){
x = 1;
v.exchange(1,std::memory_order::release); // #1
});
std::thread t2([&](){
int expected = 1;
while(!v.compare_exchange_strong(expected,3,std::memory_order::relaxed)){} // #2
v.fetch_sub(1,std::memory_order::relaxed); // #3
});
std::thread t3([&](){
int expected = 2;
while(!v.compare_exchange_strong(expected,10,std::memory_order::relaxed)){} // #4
assert(x ==1);
});
t1.join();
t2.join();
t3.join();
}
[介绍.races] p5 说:
以原子对象 M 上的释放操作 A 为首的释放序列是 M 的修改顺序中副作用的最大连续子序列,其中第一个操作是 A,后续的每个操作都是原子读-修改-写操作。
在本例中,
v
的修改顺序是{0, 1, 3, 2,10}
,因为#4
读取了#3
写入的值,并且#3
可以考虑在以#1
为首的释放顺序中,因此,断言永远不会失败,这是正确的理解吗?
您是对的,任何释放操作都可以引导释放序列,无论是普通存储还是 RMW 存储。 你的分析是正确的,(#1,#2,#3,#4)是以#1为首的释放序列,#4读取#3写入的值。
但是,您不能断定#1 与#4 同步,因为#4 不是acquire。 请记住,在尝试从获取/释放操作获得同步时,我们总是诉诸atomics.order p2:
原子操作 A 对原子对象 M 执行释放操作,与原子同步 操作 B 对 M 执行获取操作并从释放中的任何副作用中获取其值 以 A 为首的序列。
通过取 A = #1、M =
v
、B = #4 和“释放序列中的任何副作用”= #3 来满足此定义的每个元素,但 B = #4 不执行获取操作。 所以我们不能得出#1 与#4 同步的结论。 因此,t1 中 x
的存储不会发生在 t3 中 x
加载之前,反之亦然。 因此该程序包含数据竞争,并且其行为是未定义的;特别是,断言可能会失败。
#4 本身就是以 #1 为首的发布序列的一个元素,这一事实与此分析无关。 (事实上,它是 RMW 的事实与该程序无关,因为存储的值
10
从未被加载。)
不难看出程序在典型实现的实践中如何失败:由于 #4 是宽松的,编译器和/或 CPU 可以自由地使用以下负载
x
对其进行重新排序。 因此,来自 x
的加载可能会在 v
中看到更新值之前很久就发生。 特别是,没有什么可以阻止它在从 x
存储到 t1
之前发生,因此可以加载旧值 0
。
如果您将 #4 升级到
memory_order_acquire
或更强,那么您确实可以得出结论:该程序具有明确定义的行为,并且断言不会失败。 (#4 执行 RMW 仍然无关紧要;如果它是普通获取负载,结论将是相同的。)