考虑这个例子:
#include <iostream>
#include <atomic>
#include <thread>
#include <chrono>
#include <cassert>
int main(){
std::atomic<int> v = 0;
std::atomic<bool> flag = false;
std::thread t1([&](){
while(!flag.load(std::memory_order::relaxed)){} // #1
assert(v.exchange(2,std::memory_order::relaxed) == 1); // #2
});
std::thread t2([&](){
if(v.exchange(1,std::memory_order::relaxed) == 0){ // #3
flag.store(true, std::memory_order::relaxed); // #4
}
});
t1.join();
t2.join();
}
在本例中,仅当
#1
将#4
设置为flag
时,才会退出true
处的循环,仅当true
处RMW操作的读取部分时,将标志设置为
#3
读作
0
。由于RMW操作的读取部分的要求是[atomics.order] p10
原子读-修改-写操作应始终读取在与读-修改-写操作关联的写操作之前写入的最后一个值(按修改顺序)。
这意味着如果
0
处的 RMW 操作读取 #3
,则任何其他 RMW 操作都无法读取值 0
。换句话说,如果 #2
可以读取 0
并写入 2
,则 #3
不会读取 0
并且 #4
也不会被执行。换句话说,如果所有其他操作也是 RMW 操作,则读-修改-写操作的读取值由该操作唯一拥有(这就是自旋锁工作原理的本质)。
所以,Q1 是:
#2
处的断言永远不会失败,对吗?
但是,如果
#2
改成纯负载的话,是这样的:
assert(v.load(std::memory_order::relaxed) == 1); // #2'
根据 [intro.races] p18
如果原子对象 M 上的副作用 X 发生在 M 的值计算 B 之前,则评估 B 从 X 或按照 M 的修改顺序从 X 后面的副作用 Y 获取其值。
在
#2'
之前发生的副作用只是初始值0
,即使存储在1
的副作用#3
在修改顺序上位于0
之后,纯负载仍然可以读取0
因为 [intro.races] p18 使用“or”,这也是 [atomics.order] p11 所暗示的
推荐实践:实现应该使原子存储对原子加载可见,并且原子加载应该在合理的时间内观察原子存储。
从实现的角度来看,存在时间滞后,使得
#3
处的商店在合理的时间内对#2'
不可见。从C++标准的角度来看,这个结果也是[intro.races] p18中的“或”所暗示的。
问题2: 如果将
#2
处的RMW操作改为#2'
这样的纯负载,断言可能会失败,对吧?
我不认为
#2
可以由编译器用 #1
重新排序,因为 #2
类似于自旋锁中失败的 CAS(即,这是具有宽松内存顺序的纯负载),如果存在重新排序,自旋锁也将不起作用。此外,由于断言,编译器在此示例中进行的任何重新排序都是可见的。但从内存顺序来看,#3
不会先于#2
发生,反之亦然,理论上#3
可能会失败。我不知道。 但是,这个示例依赖于执行的逻辑顺序,对顺序的任何破坏都是可以观察到的。
注意:
这是与加载操作相比,原子对象的读-修改-写操作的加载部分是否保证读取修改顺序中的最后一个值?的后续问题,其中有一个不清楚的示例和一个难以理解的假设,其中在这个问题上得到了改进和清晰。
因为您正在使用
memory_order_relaxed
,所以允许编译器生成代码,这样您在 #2 处的断言可能会失败。还允许该程序永远不会终止,因为线程 t1 永远不会完成。
memory_order_relaxed
意味着它只需要确保操作是原子的,没有全局排序要求,甚至不需要对变量的宽松存储可供其他线程使用:只要没有人可以使用看到一个半更新的值,每个人都只能看到曾经存储在那里的一些值,它已经完成了您要求它做的事情。
当我输入此内容时,我相信由于编译器如何将操作映射到底层硬件以及底层硬件的一致性模型,您将无法在当前的 x86 平台上创建此场景,这仍然是事实。
如果您希望标准要求您所讨论的行为,您可能要输入的内容是:
std::thread t1([&](){
while(!flag.load(std::memory_order::acquire)){} // #1
assert(v.exchange(2,std::memory_order::acq_rel) == 1); // #2
});
std::thread t2([&](){
if(v.exchange(1,std::memory_order::acq_rel) == 0){ // #3
flag.store(true, std::memory_order::release); // #4
}
});
在 x86 上,这几乎肯定会编译为与当前代码相同的汇编指令。