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中的获取栅栏同步。因此,在t3中的任何加载之前都不会发生任何存储,因此您根本无法保证它们可能返回什么值。

栅栏真的应该在data2.store(data1)之前,然后它应该工作。我向你保证,任何注意到这个商店的人以后也会注意到以前所有的商店。这将包括t1的state.store(1),由于t1和t2之间的释放/获取对,它被提前排序。

如果你把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

则当t3中的data2.load()返回1时,t2中的释放栅栏与t3中的获取栅栏同步(参见c++ 20原子)。栅栏p2)。data2的t2 store只有在state的t2 load返回1时才会发生,这将确保t1中的释放store与t2(原子)中的获取fence同步。栅栏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()不能返回0。这将确保所需的排序,而不需要seq_cst


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

所以假设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缓存,但到那时已经太晚了。

最新更新