栅栏实际上如何在 C++ 中工作

How do fences actually work in c++

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

例如,假设我有这个代码

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) 循环,因为它不包含对 volatileatomic[ 的访问=42=] 变量并且不使用 同步 操作。 (C++ 标准说任何正确的程序都应该最终 执行上面指定的操作之一,因此编译器在优化程序时可能会依赖于该事实)。

以此类推

更一般地说,C++ 标准指定 并发 访问任何 非原子 变量会导致 未定义的行为,类似于 "Warranty is cleared"。这就是为什么你应该使用 atomic y 变量。

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