我一直在努力理解栅栏实际上是如何强制代码进行同步的。
例如说我有这个代码
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);
}
因为发布围墙之后是原子存储操作,而获取围墙之前是原子加载,所以一切都按照预期的方式同步,并且断言不会触发
但是如果y不是像这样的原子变量
bool x;
bool y;
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;
}
然后,我听说可能会有一场数据竞赛。但是为什么呢?
为什么必须在发布篱笆后面添加原子存储,并在获取篱笆之前添加原子加载,以使代码正确同步?
如果有人可以提供一个执行场景,其中数据竞争导致断言被触发,我也将不胜感激。
最佳答案
对于第二个片段,没有真正的数据争用是一个问题。如果编译器从字面上字面意义上是从编写的代码生成机器代码,那么此代码片段就可以了。
但是,编译器可以自由生成任何机器代码,对于单线程程序而言,它等效于原始代码。
例如,编译器可以注意到y
变量在while(!y)
循环内不会更改,因此它可以一次加载此变量以进行注册,并仅在下一次迭代中使用该寄存器。因此,如果最初是y=false
,您将得到一个无限循环。
另一种可能的优化方法是删除while(!y)
循环,因为它不包含对 Volatile 或原子变量的访问,并且不使用同步操作。 (C++ Standard表示,任何正确的程序都应最终执行上述指定的操作之一,因此,编译器在优化程序时可能会依赖于该事实)。
等等。
更一般地,C++标准规定对任何非原子变量的并发访问会导致未定义行为,就像“保修已清除”。这就是为什么您应该使用原子y
变量的原因。
另一方面,变量x
不需要是原子的,因为由于内存限制,对它的访问不是并发的。