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

例如说我有这个代码

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不需要是原子的,因为由于内存限制,对它的访问不是并发的。

10-06 04:08