1

我一直在努力理解栅栏实际上是如何强制代码同步的。

例如,假设我有这个代码

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;
}

然后,我听说,可能会有一场数据竞赛。但这是为什么呢?为什么释放栅栏后必须跟原子存储,而获取栅栏前必须有原子加载才能使代码正确同步?

如果有人可以提供数据竞争导致断言触发的执行场景,我也将不胜感激

4

1 回答 1

2

对于您的第二个片段,没有真正的数据竞争是一个问题。这个片段就可以了……如果编译器会从所写的代码中生成机器代码。

但是编译器可以自由生成任何机器码,相当于单线程程序的原始机器码。

例如,编译器可以注意到,该y变量在循环内不会改变while(!y),因此它可以加载该变量一次以注册并在下一次迭代中仅使用该寄存器。所以,如果最初y=false,你会得到一个无限循环。

另一种可能的优化只是删除while(!y)循环,因为它不包含对易失性原子变量的访问,也不使用同步操作。(C++ 标准规定任何正确的程序最终都应该执行上述指定的操作之一,因此编译器在优化程序时可能会依赖这一事实)。

等等。

更一般地说,C++ 标准规定对任何非原子变量的并发访问都会导致Undefined Behavior,这就像“保修已清除”。这就是为什么你应该使用原子变量。 y

另一方面,变量x不需要是原子的,因为内存栅栏对它的访问不是并发的。

于 2015-12-09T23:34:41.620 回答