hon*_* bi 5 c++ concurrency multithreading
#include <atomic>
#include <cassert>
#include <thread>
std::atomic<bool> x = false, y = false, go = false;
int v = 0;
// t1
void write_xy() {
while (!go) {
std::this_thread::yield();
}
v = 1; // 1
x.store(true, std::memory_order_relaxed); // 2
y.store(true, std::memory_order_relaxed); // 3
}
// t2
void read_yx() {
while (!go) {
std::this_thread::yield();
}
while (!y.load(std::memory_order_relaxed))
;
assert(1 == x.load(std::memory_order_relaxed)); // 4
assert(1 == v); // 5
}
int main() {
for (;;) {
x = false;
y = false;
v = 0;
go = false;
std::thread t1(write_xy);
std::thread t2(read_yx);
go = true; // start
t1.join();
t2.join();
}
}
Run Code Online (Sandbox Code Playgroud)
作为C ++并发编程的初学者,根据我的理解memory_order_relaxed,t1上述代码中线程中三个语句的执行顺序是看不到的t2。从的角度来看t2,中的三个语句t1的顺序可能为3、2、1,因此可能触发4和5的断言。
经过多次尝试后,assert从未触发,因此我写了一个无限循环来重复上述过程,assert但仍然没有触发。后来,有人怀疑它t1在t2开始执行之前就已结束,因此go引入了变量以等待两个线程的开始,以确保两个线程都尽快开始执行,并且assert仍然没有被触发。
我在装有Centos8和4个CPU的虚拟机上进行了测试。我的CPU是i5-7500。
语言允许发生某些事情并不意味着您可以在特定情况下重现它。
让我们暂时忽略数据竞争v(即使这意味着您的程序具有未定义的行为)。
您正在编译x86的代码,该代码对内置的内存排序具有非常强的保证。例如,使用std::memory_order_release以下命令执行存储时,您将获得完全相同的汇编代码:
mov DWORD PTR v[rip], 1
mov BYTE PTR x[rip], 1
mov BYTE PTR y[rip], 1
Run Code Online (Sandbox Code Playgroud)
所以这个代码(编译为你的CPU)都保证有两个v == 1和x == 1可见的,当所有其他线程y == 1。您的C ++程序没有此保证,但是此机器代码有此保证。
同样,std::memory_order_acquire用于加载无效(只有断言消息的文本更改):
movzx eax, BYTE PTR y[rip]
[...]
movzx eax, BYTE PTR x[rip]
[...]
cmp DWORD PTR v[rip], 1
Run Code Online (Sandbox Code Playgroud)
同样,该平台已经提供了必要的保证。其他平台(例如ARM)提供的保证较少,您会在编译后的二进制文件中看到差异:
在这里,同步被添加到所有存储并读取:
bl __sync_synchronize
Run Code Online (Sandbox Code Playgroud)
上面的x86代码也是为什么此时数据争用v不起作用的原因。但是,依赖于此是一个可怕的想法,因为编译器将完全有权(例如)assert(v == 1);在之前移动while (!y.load(std::memory_order_relaxed))。只是目前还没有这样做。
获得断言的另一种方法是,如果编译器重新排序了您的加载和存储。可以这样做(但是上面的发布获取顺序则不允许这样做),但事实并非如此,因为这样做没有意义。您可能可以通过更改周围的代码来诱使这样做,但是我无法提出一种实现方法。