我有一个使用std::atomic_thread_fence
s的程序:
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缓存,但到那时已经太晚了。