memory_order_seq_cst和memory_order_acq_rel有何不同?

APr*_*mer 30 c++ memory-model c++11 stdatomic

存储是释放操作,负载是两者的获取操作.我知道这memory_order_seq_cst意味着要为所有操作强加一个额外的总排序,但是我没有建立一个例子,如果所有的操作memory_order_seq_cst都被替换,那就不是这样了memory_order_acq_rel.

我是否会遗漏某些内容,或者差异仅仅是文档效果,即memory_order_seq_cst如果有人打算不使用更轻松的模型并且memory_order_acq_rel在约束轻松模型时使用,则应该使用?

MSN*_*MSN 30

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);
Run Code Online (Sandbox Code Playgroud)

操作z由两个原子变量保护,而不是一个,因此您不能使用获取释放语义来强制执行z总是递增的.

  • @CandyChiu 使用 ack_rel,`c()` 可以感知到 `a()` 中的 `x=true;` 发生在 `y=true;` 之前,同时 `d()` 中的`d()` 可以感知`y=true;` 发生在 `x=true;` 之前(由于缺乏“全局排序”。)特别是 `c()` 可以感知到 `x==true` 和 `y==false` 在同时`d()`可以感知到`y==true`和`x==false`。所以 `z` 可能不会被 `c()` 或 `d()` 增加。使用 seq_cst,如果 `c()` 感知到 `x=true;` 发生在 `y=true;` 之前,那么 `d()` 也是如此。 (4认同)
  • @ acidzombie24,即使在这种情况下,`z`也将是2。 (2认同)
  • @nodakai,你的解释是准确的,但我认为“发生在之前”这句话可能会产生误导,因为获取-释放问题的症结在于两者都不会在对方之前发生。 (2认同)
  • 此示例使用纯加载和纯存储,而不是任何可以使用“std::memory_order_acq_rel”的实际 RMW 操作。在原子读-修改-写中,加载和存储是捆绑在一起的,因为它们是原子的。我不确定“acq_rel”何时可以与“seq_cst”不同,例如“.fetch_add”或“.compare_exchange_weak” (2认同)

Pet*_*des 11

在像 x86 这样原子映射到屏障的 ISA 上,实际的机器模型包括一个存储缓冲区:

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

  • acquire或者release根本具备刷新存储缓冲区。正常的 x86 加载和存储本质上具有 acq 和 rel 语义。(seq_cst 加上带有存储转发的存储缓冲区。)

    但是 x86 原子 RMW 操作总是被提升,seq_cst因为 x86 asmlock前缀是一个完整的内存屏障。其他 ISA 可以acq_rel在 asm 中进行放松或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_relmo_acq_rel比 更便宜mo_seq_cst,但它们seq_cst不能像 AArch64 那样便宜;seq-cst 存储需要一个完整的屏障。

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

  • @PeterCordes - 我什至不认为这是假设的:我认为这就是原子操作(有时)在当前 Intel x86 上实现的方式。也就是说,他们以乐观锁定状态加载缓存行,执行 RMW 的“前端”(包括 ALU 操作),然后在 RMW 的“后端”验证执行中的一切是否正常。 -at-retire 操作确保所有排序。当位置不存在竞争时,这非常有效。如果这多次失败,预测者将切换模式以在退休时完成整个事情,这会导致管道中出现更大的泡沫(因此是“有时”)。 (2认同)

pve*_*jer 5

尝试仅使用获取/释放语义构建 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        
}
Run Code Online (Sandbox Code Playgroud)

如果没有 [StoreLoad] 栅栏,存储可能会跳到负载前面,然后算法就会崩溃。2 个线程同时会看到另一个锁空闲,设置自己的锁并继续。现在临界区内有 2 个线程。