考虑下面的例子。假设
barrier
初始化为 0。
有一个生产者线程和两个消费者线程不断检查
barrier
。如果设置了障碍,它们就会减少runcnt
。生产者线程等待 runcnt
达到 0。我对生产者内部多个存储操作的顺序感到困惑。
如果顺序像写的那样,我认为代码会按预期运行。但是,如果
barrier
存储在 runcnt
存储之前重新排序,则断言检查似乎会失败。
我错过了什么吗?有办法解决这个问题吗?
extern atomic<int> barrier[2];
atomic_int runcnt{0};
void producer() {
runcnt.store(2, memory_order_relaxed);
barrier[0].store(1, memory_order_relaxed);
barrier[1].store(1, memory_order_relaxed);
while (runcnt.load(memory_order_relaxed)) {
cpu_pause();
}
}
void consumer(unsigned index) {
while (true) {
if (barrier[index].exchange(false, memory_order_relaxed)) {
int prev = runcnt.fetch_sub(1, memory_order_relaxed);
assert(prev > 0);
}
}
}
正如 @peter-cordes 指出的,与单向的常规发布存储不同,
std::atomic_thread_fence(release)
充当存储之间的双向屏障(负载仍然可以重新排序)。
因此,这是固定代码:
void producer() {
runcnt.store(2, memory_order_relaxed);
std::atomic_thread_fence(release); // <- Prevents reordering of barrier stores before runcnt.store.
barrier[0].store(1, memory_order_relaxed);
barrier[1].store(1, memory_order_relaxed);
while (runcnt.load(memory_order_relaxed)) {
cpu_pause();
}
}
如果顺序与所写的一样 - 您的意思是如果操作碰巧以
seq_cst
也允许的顺序变得可见?当然,您可以在所有操作中使用 seq_cst
来要求这一点。
我认为读者方面的最小值是
barrier[i].exchange
为 acquire
。
在编写器方面,两个
barrier[i]
存储都需要是release
,或者在std::atomic_thread_fence(release)
之后放置一个runcnt.store
,所以它位于它和任一障碍存储之间。
这使得
exchange
与它加载的任何 barrier
存储同步,假设它加载了 1
,因此 if
主体完全运行。
runcnt.store(relaxed)
; barrier[0].store(release)
;在 C++ 内存模型中,甚至在针对某些 ISA 进行编译时,barrier[1].store(relaxed)
不够:最终的宽松存储可以通过发布存储,因为它只是单向屏障。这是栅栏和操作之间的关键区别:https://preshing.com/20131125/acquire-and-release-fences-dont-work-the-way-youd-expect/。即使做中间存储seq_cst
也是不够的,它仍然只是一个操作,而不是2向围栏。