我一直在努力了解围栏实际上是如何迫使代码同步的。
例如,说我有这个代码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;
}
然后,我听说,可能会有数据竞争。但那是为什么呢? 为什么必须释放围栏后面跟一个原子库,并且获取围栏前面有一个原子加载,以便代码正确同步?
如果有人能提供数据竞争导致断言触发的执行方案,我也将不胜感激
答案 0 :(得分:2)
没有真正的数据竞争是第二个代码段的问题。如果编译器字面从编写的代码生成机器代码,那么这段代码就没问题了。
但编译器可以自由生成任何机器代码,这相当于单线程程序时的原始机器代码。
例如,编译器可以注意到y
变量在while(!y)
循环内没有变化,所以它可以加载一次该变量来注册并在下一次迭代中仅使用该寄存器。因此,如果最初y=false
,您将获得无限循环。
另一种可能的优化就是删除while(!y)
循环,因为它不包含对 volatile 或 atomic 变量的访问权限。不使用同步操作。 (C ++标准认为任何正确的程序都应最终执行上面指定的操作之一,因此编译器在优化程序时可能依赖于这一事实。)
等等。
更一般地说,C ++标准指定对任何非原子变量的并发访问导致未定义行为,这就像&# 34;保修已清除&#34;。这就是为什么你应该使用 atomic y
变量。
另一方面,变量x
不需要是原子的,因为对它的访问因内存栅栏而不是并发的。