在 C++ 中使用 acquire/release fence 的可见操作顺序

Visible order of operations with acquire/release fence in C++

我有一个使用 std::atomic_thread_fences 的以下程序:

int data1 = 0;
std::atomic<int> data2 = 0;
std::atomic<int> state;

int main() {
    state.store(0);
    data1 = 0;
    data2 = 0;

    std::thread t1([&]{
        data1 = 1;
        state.store(1, std::memory_order_release);
    });

    std::thread t2([&]{
        auto s = state.load(std::memory_order_relaxed);
        if (s != 1) return;

        std::atomic_thread_fence(std::memory_order_acquire);

        data2.store(data1, std::memory_order_relaxed);

        std::atomic_thread_fence(std::memory_order_release);
        state.store(2, std::memory_order_relaxed);
    });

    std::thread t3([&]{
        auto d = data2.load(std::memory_order_relaxed);
    
        std::atomic_thread_fence(std::memory_order_acquire);

        if (state.load(std::memory_order_relaxed) == 0) {
            std::cout << d;
        }
    });

    t1.join();
    t2.join();
    t3.join();
}

它由3个线程和一个用于同步的全局原子变量state组成。第一个线程将一些数据写入全局非原子变量 data1 并将 state 设置为 1。第二个线程读取 state 并且如果它等于 1 它修改赋值 data1 到另一个全局非原子变量 data2。之后,它将 2 存储到 state 中。该线程读取 data2 的内容,然后检查 state.

问:第三个线程会一直打印0吗?或者第三个线程是否有可能在更新到 state 之前看到对 data2 的更新?如果是这样,是保证使用 seq_cst 内存顺序的唯一解决方案吗?

我认为t3可以打印1。

我认为基本问题是 t2 中的释放栅栏放错了位置。它应该在要“升级”到发布的商店之前排序,以便所有较早的加载和商店在后来的商店之前变得可见。在这里,它具有“升级” state.store(2) 的效果。但这没有帮助,因为没有人试图使用条件 state.load() == 2 来订购任何东西。所以 t2 中的释放栅栏与 t3 中的获取栅栏不同步。因此,您不会对 happen-before 中的任何负载进行任何存储,因此您根本无法保证它们可能 return.

的值

围栏确实应该在 data2.store(data1) 之前,然后它应该会起作用。您可以放心,观察该存储的任何人此后都会观察所有先前的存储。这将包括 t1 的 state.store(1),它由于 t1 和 t2 之间的 release/acquire 对而较早订购。

因此,如果您将 t2 更改为

        auto s = state.load(std::memory_order_relaxed);
        if (s != 1) return;

        std::atomic_thread_fence(std::memory_order_acquire);

        std::atomic_thread_fence(std::memory_order_release); // moved
        data2.store(data1, std::memory_order_relaxed);

        state.store(2, std::memory_order_relaxed);  // irrelevant

然后每当 data2.load() 在 t3 returns 1 时,t2 中的释放栅栏与 t3 中的获取栅栏同步(参见 C++20 atomics.fences p2)。 data2 的 t2 存储仅在 state returned 1 的 t2 加载时发生,这将确保 t1 中的发布存储与 t2 中的获取栅栏同步(atomics.fences p4).然后我们有

t1 state.store(1)
    synchronizes with
t2 acquire fence
    sequenced before
t2 release fence
    synchronizes with
t3 acquire fence
    sequenced before
t3 state.load()

所以 state.store(1) 发生在 state.load() 之前,因此 state.load() 在这种情况下不能 return 0。这将确保所需的顺序而不需要 seq_cst.


要想象原始代码实际上是如何失败的,想想像 POWER 这样的东西,其中某些核心集在它们命中 L1 缓存并变得全局可见之前,可以从彼此的存储缓冲区中获得对窥探存储的特殊早期访问。然后 acquire barrier 只需要等到所有早期的加载都完成;而释放屏障不仅应该耗尽它自己的存储缓冲区,还应该耗尽它有权访问的所有其他存储缓冲区。

所以假设core1和core2是一对特殊的,但是core3更远,只有写入L1缓存后才能看到存储。我们可以:

core1                  core2          L1 cache     core3
=====                  =====          ========     =====
data1 <- 1
release                               data1 <- 1
state <- 1
(still in store buffer)
                      1 <- state
                      acquire
                      1 <- data1
                      data2 <- 1      data2 <- 1
                                                  1 <- data2
                                                  acquire
                                                  0 <- state
                      release         state <- 1
                      state <- 2      state <- 2

核心 2 中的释放屏障确实导致核心 1 的存储缓冲区耗尽,从而将 state <- 1 写入 L1 缓存,但到那时为时已晚。