man*_*652 14 c++ multithreading
我正在查看Antony Williams 的 C++ Concurrency in Action 中的列表 5.13:,我对“存储和加载y仍然必须是原子的;否则,将会出现数据竞争y”的评论感到困惑。这意味着如果y是一个普通(非原子)布尔值,那么断言可能会触发,但为什么呢?
#include <atomic>
#include <thread>
#include <assert.h>
bool x=false;
std::atomic<bool> y;
std::atomic<int> z;
void write_x_then_y()
{
x=true;
std::atomic_thread_fence(std::memory_order_release);
y.store(true,std::memory_order_relaxed);
}
void read_y_then_x()
{
while(!y.load(std::memory_order_relaxed));
std::atomic_thread_fence(std::memory_order_acquire);
if(x) ++z;
}
int main()
{
x=false;
y=false;
z=0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load()!=0);
}
Run Code Online (Sandbox Code Playgroud)
现在让我们更改y为普通布尔值,我想了解为什么断言可以触发。
#include <atomic>
#include <thread>
#include <assert.h>
bool x=false;
bool y=false;
std::atomic<int> z;
void write_x_then_y()
{
x=true;
std::atomic_thread_fence(std::memory_order_release);
y=true;
}
void read_y_then_x()
{
while(!y);
std::atomic_thread_fence(std::memory_order_acquire);
if(x) ++z;
}
int main()
{
x=false;
y=false;
z=0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load()!=0);
}
Run Code Online (Sandbox Code Playgroud)
我知道数据竞争发生在非原子全局变量上,但在这个例子中,如果 while 循环退出read_y_then_x,我的理解是y必须已经设置为 true,或者正在设置为 true 的过程中(因为它是write_x_then_y线程中的非原子操作)。由于atomic_thread_fence在write_x_then_y线程中确保上面编写的代码没有可以在之后重新排序,因此我认为该x=true操作一定已经完成。另外,两个线程中的std::memory_order_release和std::memory_order_acquire标签确保 的更新值在读取 时x已与线程同步,所以我觉得断言仍然有效......我错过了什么?read_y_then_xx
use*_*522 12
在不同步的两个线程中访问非原子对象(其中一个访问是写访问)始终是一种数据竞争,并会导致未定义的行为。这就是术语“数据竞争”在 C++ 语言中的正式定义及其后果。它不仅仅是一种竞争条件,非正式地指由于某些线程访问的未指定顺序而允许多种可能的结果。
写入y=true;发生在循环while(!y);仍在读取时,如果是非原子的,y则这会导致数据竞争。y该程序将具有未定义的行为,这不仅仅意味着可能assert会触发。这意味着程序可能会执行任何操作,例如崩溃或冻结。
编译器可以在这种情况永远不会发生的假设下进行优化,从而以不保留预期行为的方式优化代码,因为它依赖于导致数据争用的访问。
此外,最终不执行任何原子/同步/易失性/IO 操作的无限循环也具有未定义的行为。如果不是原子,则while(!y);具有未定义的行为,并且最初编译器可以假设该行在这些条件下无法访问。yfalse
例如,编译器可以出于这个原因从函数中删除循环,就像当前编译器实际发生的那样,请参阅问题下的注释。
而且我还知道,特别是 Clang 确实会基于此执行优化,有时甚至会完全删除ret具有这样一个无限循环的发出函数中的所有内容(包括最后的指令!),如果它永远无法实现的话被调用时没有未定义的行为。然而在这里,因为y可能是true在调用函数时,在这种情况下没有未定义的行为,所以这种情况不会发生。
所有这些都是在语言层面上的。如果程序是以最直译的方式编译的,那么硬件级别上会发生什么并不重要。这些将是额外的问题,例如,潜在的写访问撕裂和线程之间潜在的缓存不一致,但这两者在bool. 另一个问题可能是线程可以在寄存器中保留变量的副本,可能永远不会生成其他线程可以观察到的存储,这对于非非对象是允许atomic的volatile。
如果你这样写:
bool y=false;
...
while(!y);
Run Code Online (Sandbox Code Playgroud)
那么编译器可以假设y它本身不会改变。的主体while是空的,所以要么y在开始时为 true 并且有一个无限循环,要么y在开始时为 false 并且while结束时为 false。
编译器可以将其优化为:
if (!y) while(true);
Run Code Online (Sandbox Code Playgroud)
但c++也说必须总是有进展,无限循环是UB,所以编译器可以在看到 a 时做任何它喜欢的事情while(true);,包括删除它。gcc 和 clang 实际上会做到这一点,正如 Jerome 在这里指出的: https: //godbolt.org/z/ocrxnee8T
所以它std::atomic<bool> y;所做的是标记为易失性的现代形式y。编译器不能再假设重复读取会y给出相同的结果,并且不能再优化while(!y);循环。
根据体系结构,它还会插入必要的内存屏障,以便其他线程可以观察到变量的更改,这比 易失性要多。