memory_order_seq_cst 和 memory_order_acq_rel 有何不同?

Posted

技术标签:

【中文标题】memory_order_seq_cst 和 memory_order_acq_rel 有何不同?【英文标题】:How do memory_order_seq_cst and memory_order_acq_rel differ? 【发布时间】:2012-09-02 16:12:30 【问题描述】:

存储是释放操作,加载是两者的获取操作。我知道memory_order_seq_cst 旨在为所有操作强加一个额外的总排序,但我无法构建一个示例,如果所有memory_order_seq_cst 都被memory_order_acq_rel 替换,情况并非如此。

我是否遗漏了什么,或者差异只是文档效果,即如果一个人不打算使用更宽松的模型,应该使用memory_order_seq_cst,而在约束宽松模型时使用memory_order_acq_rel

【问题讨论】:

【参考方案1】:

http://en.cppreference.com/w/cpp/atomic/memory_order 有一个很好的例子at the bottom,它只适用于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 始终递增。

【讨论】:

@acidzombie24,即使在这种情况下,z 也会是 2。 @CandyChiu 使用ack_rel,c()可以感知a()中的x=true;发生在y=true;之前b()同时d()可以感知到y=true;发生在之前x=true;(由于缺少“全局排序”。)特别是c()可以同时感知x==truey==falsed()可以感知y==truex==false。所以z 可能不会增加c()d()。使用 seq_cst,如果 c() 感知到 x=true; 发生在 y=true; 之前,那么 d() 也是如此。 @MSN 你的意思是int z=0,而不是bool z=0 @nodakai,您的解释是准确的,但我认为“发生在之前”这句话可能会产生误导,因为获取释放问题的症结在于两者都没有写入 happen-before 另一个。 此示例使用纯加载和纯存储,而不是任何可以使用std::memory_order_acq_rel 的实际 RMW 操作。在原子读取-修改-写入中,加载和存储是绑定在一起的,因为它们是原子的。我不确定acq_rel 是否会与seq_cst 不同,例如.fetch_add.compare_exchange_weak【参考方案2】:

在 x86 等 ISA 上,原子映射到障碍,实际机器模型包括存储缓冲区:

seq_cst 存储需要刷新存储缓冲区,因此该线程稍后的读取会延迟到存储全局可见之后。

acquirerelease 必须刷新存储缓冲区。正常的 x86 加载和存储本质上具有 acq 和 rel 语义。 (seq_cst 加上一个带有存储转发的存储缓冲区。)

但是 x86 原子 RMW 操作总是被提升为 seq_cst,因为 x86 asm lock 前缀是一个完整的内存屏障。其他 ISA 可以在 asm 中轻松或 acq_rel RMW,商店方面能够与以后的商店进行有限的重新订购。 (但不是以使 RMW 看起来非原子的方式:For purposes of ordering, is atomic read-modify-write one operation or two?)


https://preshing.com/20120515/memory-reordering-caught-in-the-act 是 seq_cst 存储和普通发布存储之间区别的一个有启发性的示例。(实际上是 mov + mfence 与 x86 asm 中的普通 mov。实际上,xchg 是在大多数 x86 CPU 上进行 seq_cst 存储的更有效方式,但 GCC 确实使用 mov+mfence)


有趣的事实:AArch64 的 LDAR 获取-加载指令实际上是一个顺序-获取,与 STLR 有特殊的交互。直到 ARMv8.3 LDAPR 才能 arm64 执行普通的获取操作,这些操作可以使用早期版本和 seq_cst 存储 (STLR) 重新排序。 (seq_cst 加载仍然使用 LDAR,因为它们 need that interaction with STLR 来恢复顺序一致性;seq_cstrelease 存储都使用 STLR。

使用 STLR / LDAR,您可以获得顺序一致性,但只需要在下一个 LDAR 之前排空存储缓冲区,而不是在每个 seq_cst 存储之后在其他操作之前立即排空。我认为真正的 AArch64 硬件确实以这种方式实现它,而不是在提交 STLR 之前简单地耗尽存储缓冲区。

使用 LDAR / STLR 将 rel 或 acq_rel 增强为 seq_cst 并不需要很昂贵,除非您 seq_cst 存储一些内容,然后 seq_cst 加载其他内容。那么它和 x86 一样糟糕。

其他一些 ISA(如 PowerPC)有更多的壁垒选择,可以比 mo_seq_cst 更便宜地强化到 mo_relmo_acq_rel,但它们的 seq_cst 不能像 AArch64 一样便宜; seq-cst 商店需要一个完整的屏障。

因此,AArch64 是 seq_cst 存储在现场耗尽存储缓冲区的规则的一个例外,无论是使用特殊指令还是之后的屏障指令。 ARMv8 是在 C++11 / Java / 等之后设计的 并非巧合。基本上将 seq_cst 作为无锁原子操作的默认值,因此使它们高效很重要。在 CPU 架构师花了几年时间考虑提供屏障指令或只是获取/释放与宽松的加载/存储指令的替代方案之后。

【讨论】:

"但是 x86 原子 RMW 操作总是被提升为 seq_cst,因为 x86 asm 锁定前缀是一个完整的内存屏障。" 是什么让你说它们被“提升”了? exec 也可以很好地推测性地加载值(通常)并进行计算,只要它稍后安全地重新加载(锁定加载)即可;如果计算速度很快,那可能很无趣但仍然可能。 (我认为英特尔以纯粹描述性的方式记录了这些内容,用于现有设计,而不是未来设计。) @curiousguy:x86 lock 前缀的全内存屏障特性由 Intel 和 AMD 在其 x86 ISA 手册中仔细记录。 (Does lock xchg have the same behavior as mfence?)。对于未来的 x86 CPU,它绝对有保证;编译器还能如何制作安全的面向未来的 asm?这就是我的意思是编译器必须加强所有 RMW 操作到 asm 中的 seq_cst,在 RMW 执行它的操作之前耗尽存储缓冲区。 究竟有什么保证? xdiv(如果 FPU 决定支持 RMW,则为 xcos),CPU 不会尝试获取已加载的值并提前在内存中准备好计算,因此加快代价高昂的 RMW? @curiousguy:但无论如何,如果一个假设的实现想要尝试提前加载以设置更便宜的原子交换来实际实现 RMW,它只能推测地这样做并在错误推测时回滚(如果在架构上允许负载之前更改线路)。常规负载已经以这种方式工作,在保持强大负载顺序的同时获得性能。 (请参阅machine_clears.memory_ordering 性能计数器:Why flush the pipeline for Memory Order Violation caused by other logical processors?) @PeterCordes - 我什至不认为这是假设性的:我认为这就是在当前英特尔 x86 上(有时)实现原子操作的方式。也就是说,他们以乐观锁定状态加载缓存行,执行 RMW 的“前端”(包括 ALU 操作),然后在 RMW 的“后端”中验证执行中的一切正常-at-retire 操作,确保所有排序。当位置没有竞争时,这很有效。如果这失败了很多,预测器将在退休时切换模式以完成整个事情,这会导致管道中出现更大的泡沫(因此“有时”)。【参考方案3】:

尝试仅使用获取/释放语义构建 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 个线程。

【讨论】:

【参考方案4】:

仍然使用memory_order 中的定义和示例。但是将 memory_order_seq_cst 替换为 store 中的 memory_order_release 和 load 中的 memory_order_acquire。

Release-Acquire ordering 保证了在一个线程中的 store 之前发生的所有事情在执行加载的线程中成为可见的副作用。但在我们的示例中,在 thread0 和 thread1 中 store 之前什么都没有发生。

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 都保持为 false,因此 ++z 永远不会被触及,z 保持 0 并且断言触发。

但是,如果 memory_order_seq_cst 进入图片,它会为所有被标记的原子操作建立一个单一的总修改顺序。因此,在 thread2 中,x.load 然后 y.load;在 thread3 中,y.load 然后 x.load 是确定的事情。

【讨论】:

以上是关于memory_order_seq_cst 和 memory_order_acq_rel 有何不同?的主要内容,如果未能解决你的问题,请参考以下文章

GCC 使用 `memory_order_seq_cst` 跨负载重新排序。这是允许的吗?

为啥在已经使用 seq_cst CAS 的无锁队列中需要 atomic_thread_fence(memory_order_seq_cst)?

C++内存序

11.开发newapp个人中心pages/me/me.vue和修改密码功能

忘记了电信路由器悦me账号和密码

在 VBA 中显示 Me.ComBox.Value 和 Me.ComboBox.RowSource 属性的设置