存储是释放操作,加载是两者的获取操作。我知道这memory_order_seq_cst
意味着对所有操作施加额外的总排序,但我无法构建一个示例,如果所有memory_order_seq_cst
操作都替换为memory_order_acq_rel
.
我是否遗漏了什么,或者差异只是文档效果,即memory_order_seq_cst
如果一个人不打算使用更宽松的模型并memory_order_acq_rel
在约束宽松模型时使用,则应该使用?
存储是释放操作,加载是两者的获取操作。我知道这memory_order_seq_cst
意味着对所有操作施加额外的总排序,但我无法构建一个示例,如果所有memory_order_seq_cst
操作都替换为memory_order_acq_rel
.
我是否遗漏了什么,或者差异只是文档效果,即memory_order_seq_cst
如果一个人不打算使用更宽松的模型并memory_order_acq_rel
在约束宽松模型时使用,则应该使用?
http://en.cppreference.com/w/cpp/atomic/memory_order在底部有一个很好的例子,它只适用于memory_order_seq_cst
. 本质上memory_order_acq_rel
提供相对于原子变量的读写顺序,同时memory_order_seq_cst
提供全局读写顺序。也就是说,顺序一致的操作在所有线程中以相同的顺序可见。
该示例归结为:
bool x= false;
bool y= false;
int z= 0;
a() { x= true; }
b() { y= true; }
c() { while (!x); if (y) z++; }
d() { while (!y); if (x) z++; }
// kick off a, b, c, d, join all threads
assert(z!=0);
操作z
由两个原子变量保护,而不是一个,因此您不能使用获取-释放语义来强制执行z
始终递增的操作。
在像 x86 这样的 ISA 上,原子映射到屏障,实际的机器模型包括一个存储缓冲区:
seq_cst
存储需要刷新存储缓冲区,因此该线程稍后的读取会延迟到存储全局可见之后。
acquire
或者release
不必刷新存储缓冲区。正常的 x86 加载和存储本质上具有 acq 和 rel 语义。(seq_cst 加上一个带有存储转发的存储缓冲区。)
但是 x86 原子 RMW 操作总是被提升为,seq_cst
因为 x86 asmlock
前缀是一个完整的内存屏障。其他 ISA 可以在 asm 中进行轻松或acq_rel
RMW,商店方能够与以后的商店进行有限的重新排序。(但不是以使 RMW 看起来非原子的方式:为了排序,原子读取-修改-写入是一个还是两个操作?)
https://preshing.com/20120515/memory-reordering-caught-in-the-act是 seq_cst 存储和普通发布存储之间区别的一个有启发性的示例。 (在 x86 asm 中它实际上是mov
+mfence
与普通mov
。实际上xchg
是在大多数 x86 CPU 上执行 seq_cst 存储的更有效方法,但 GCC 确实使用mov
+mfence
)
有趣的事实:AArch64 的 LDAR 获取加载指令实际上是一个顺序获取,与 STLR 有特殊的交互。直到 ARMv8.3 LDAPR 才能 arm64 执行普通的获取操作,这些操作可以使用早期版本和 seq_cst 存储 (STLR) 重新排序。(seq_cst
加载仍然使用 LDAR,因为它们需要与 STLR 的交互来恢复顺序一致性;seq_cst
并且release
存储都使用 STLR)。
使用 STLR / LDAR 您可以获得顺序一致性,但只需在下一个 LDAR 之前耗尽存储缓冲区,而不是在每个 seq_cst 存储之后立即在其他操作之前。我认为真正的 AArch64 硬件确实以这种方式实现它,而不是在提交 STLR 之前简单地耗尽存储缓冲区。
使用 LDAR / STLR 将 rel 或 acq_rel 加强到 seq_cst 不需要很昂贵,除非你 seq_cst 存储一些东西,然后 seq_cst 加载其他东西。然后它和x86一样糟糕。
其他一些 ISA(如 PowerPC)有更多的屏障选择,并且可以比 更便宜地增强mo_rel
,但它们不能像 AArch64 那样便宜;seq-cst 商店需要一个完整的屏障。mo_acq_rel
mo_seq_cst
seq_cst
因此,AArch64 是一个例外,即seq_cst
存储在现场耗尽存储缓冲区的规则,无论是使用特殊指令还是之后的屏障指令。ARMv8 是在C++11 / Java / 等之后设计的,这并非巧合。基本上确定 seq_cst 作为无锁原子操作的默认值,因此使它们高效很重要。在 CPU 架构师有几年时间考虑提供屏障指令或只是获取/释放与宽松的加载/存储指令的替代方案之后。
尝试仅使用获取/释放语义构建 Dekkers 或 Petersons 算法。
这是行不通的,因为获取/释放语义不提供 [StoreLoad] 栅栏。
在 Dekkers 算法的情况下:
flag[self]=1 <-- STORE
while(true){
if(flag[other]==0) { <--- LOAD
break;
}
flag[self]=0;
while(turn==other);
flag[self]=1
}
如果没有 [StoreLoad] 栅栏,商店可能会跳到负载前面,然后算法就会中断。2个线程同时看到另一个锁是空闲的,设置自己的锁并继续。现在您在关键部分中有 2 个线程。
仍然使用memory_order的定义和示例。但是将 memory_order_seq_cst 替换为 store 中的 memory_order_release 和 load 中的 memory_order_acquire。
Release-Acquire 顺序保证了在一个线程中的存储成为执行加载的线程中可见的副作用之前发生的一切。但是在我们的示例中,在存储在 thread0 和 thread1 之前什么都没有发生。
x.store(true, std::memory_order_release); // thread0
y.store(true, std::memory_order_release); // thread1
此外,如果没有 memory_order_seq_cst,则无法保证 thread2 和 thread3 的顺序。你可以想象它们变成:
if (y.load(std::memory_order_acquire)) { ++z; } // thread2, load y first
while (!x.load(std::memory_order_acquire)); // and then, load x
if (x.load(std::memory_order_acquire)) { ++z; } // thread3, load x first
while (!y.load(std::memory_order_acquire)); // and then, load y
因此,如果 thread2 和 thread3 在 thread0 和 thread1 之前执行,这意味着 x 和 y 都保持为假,因此,++z 永远不会被触及,z 保持 0 并且断言触发。
但是,如果 memory_order_seq_cst 进入图片,它会为所有被标记的原子操作建立一个单一的总修改顺序。因此,在 thread2 中,x.load 然后 y.load;在 thread3 中,y.load 然后 x.load 是确定的事情。