Dekker式同步的失败通常通过重新排序指令来解释。即,如果我们写
atomic_int X;
atomic_int Y;
int r1, r2;
static void t1() {
X.store(1, std::memory_order_relaxed)
r1 = Y.load(std::memory_order_relaxed);
}
static void t2() {
Y.store(1, std::memory_order_relaxed)
r2 = X.load(std::memory_order_relaxed);
}
然后可以使用商店对加载进行重新排序,从而导致r1==r2==0
。
我期待一个acquire_release围栏来防止这种重新排序:
static void t1() {
X.store(1, std::memory_order_relaxed);
atomic_thread_fence(std::memory_order_acq_rel);
r1 = Y.load(std::memory_order_relaxed);
}
static void t2() {
Y.store(1, std::memory_order_relaxed);
atomic_thread_fence(std::memory_order_acq_rel);
r2 = X.load(std::memory_order_relaxed);
}
负载不能移动到围栏上方,并且商店不能移动到围栏下面,因此应该防止不良结果。
然而,实验表明r1==r2==0
仍然可能发生。是否有基于重新排序的解释?我的推理中存在哪些缺陷?
答案 0 :(得分:8)
根据我的理解(主要来自阅读Jeff Preshings blog),atomic_thread_fence(std::memory_order_acq_rel)
会阻止除StoreLoad
之外的任何重新排序,即它仍允许对Store
重新排序随后的Load
。但是,这正是您的示例中必须要防止的重新排序。
更准确地说,atomic_thread_fence(std::memory_order_acquire)
会阻止任何后续Load
与之后的任何Store
以及任何后续Load
的重新排序,即它会阻止LoadLoad
和LoadStore
在篱笆上重新排序。
atomic_thread_fence(std::memory_order_release)
会阻止任何后续Store
与任何前面的Store
和任何前面的Load
进行重新排序,即它会阻止LoadStore
和{{1跨越篱笆重新排序。
然后StoreStore
会阻止联合,即它会阻止atomic_thread_fence(std::memory_order_acq_rel)
,LoadLoad
和LoadStore
,这意味着只有StoreStore
可能仍会发生。
答案 1 :(得分:4)
memory_order_acq_rel
实际上就像在同一个地方获取和释放围栏一样。但问题是它们并没有阻止所有可能的重新排序,它们防止随后的负载或先前的存储被重新排序在围栏周围。因此,先前的装载和随后的商店仍然可以通过围栏。
在Dekker同步中,重要的是要防止例如在另一个线程中存储之前,即在栅栏之前重新排序的负载。 现在,展开这种同步发生的循环,你会得到前一次迭代的负载可以通过当前迭代的栅栏落下。
memory_order_seq_cst
适用于Dekker的同步,因为它可以阻止任何重新排序。例如,tbb使用Dekker的算法和mfence
来进行工作窃取。
为了更好地理解,请参阅Herb Sutter讲座中的精彩动画“Atomic<> weapons 1/2”,时间为0:43。