使用MEMORY_ORDER_SEQ_CST和Memory_ORDER_RELEASE的可能排序

参考以下代码

auto x = std::atomic<std::uint64_t>{0};
auto y = std::atomic<std::uint64_t>{0};

// thread 1
x.store(1, std::memory_order_release);
auto one = y.load(std::memory_order_seq_cst);

// thread 2
y.fetch_add(1, std::memory_order_seq_cst);
auto two = x.load(std::memory_order_seq_cst);

这里是否可能onetwo都是0?


(我似乎遇到了一个错误,如果onetwo在上面的代码运行后都能保持值0,就可以解释这个错误。而且排序规则太复杂,我无法计算出上面可能进行的排序。)


解决方案

是的,两个加载都可能获得0

在线程1中,y.load可以传递x.store(mo_release),因为它们不都是seq_cst。ISO C++保证的seq_cst操作的全局总顺序仅包括seq_cst操作。

(就正常CPU的硬件/CPU体系结构而言,在释放存储离开存储缓冲区之前,加载可以从一致缓存中获取一个值。在本例中,我发现根据我如何知道它是针对x86(或to generic release and acquire operations)编译的,然后应用ASM内存排序规则来进行推理要容易得多。应用这一推理假设正常的C++->asm mappings是安全的,并且至少始终与C++内存模型一样强大。如果您能够以这种方式找到合法的重新排序方式,您就不需要费力地通过C++形式主义。但如果不这样做,当然不能证明它在C++抽象机中是安全的。)

无论如何,要认识到的关键点是,seq_cst操作不像atomic_thread_fence(mo_seq_cst)-单个seq_cst操作只需恢复/维护与其他seq_cst操作交互的顺序一致性,而不是普通的获取/释放/acq_rel。(同样,获取和释放栅栏是更强的双向障碍,与获取和释放操作ASJeff Preshing explains不同。)


实现这一点的重新排序

这是唯一可能的重新排序;其他可能只是两个线程的程序顺序的交错。让商店最后出现会导致0, 0结果。

我将onetwo重命名为r1r2(每个线程中的本地寄存器和寄存器),以避免写入类似one == 0的内容。

// x=0 nominally executes in T1, but doesn't have to drain the store buffer before later loads
auto r1 = y.load(std::memory_order_seq_cst);   // T1b             r1 = 0 (y)
         y.fetch_add(1, std::memory_order_seq_cst);      // T2a   y = 1 becomes globally visible
         auto r2 = x.load(std::memory_order_seq_cst);    // T2b   r2 = 0 (x)
x.store(1, std::memory_order_release);         // T1a             x = 0 eventually becomes globally visible

这在x86上的实践中可能会发生,但有趣的是AArch64不会。X86可以在没有额外障碍的情况下执行释放存储(只需普通存储),并且seq_cst加载的编译方式与普通获取相同,仅为普通加载。

在AArch64上,Release和seq_cst存储使用STLR。SEQ_CST加载使用与STLR有特殊交互的LDAR,在最后一个STLR从存储缓冲区排出之前,不允许读取缓存。因此,ARMv8上的Release-store/seq_cst加载与seq_cst store/seq_cst加载相同。(ARMv8.3添加了LDAPR,允许以不同方式编译获取加载,从而实现真正的获取/释放;请参阅this Q&A。)

然而,它也可能发生在许多使用单独屏障指令的ISA上,如ARM32:发布存储通常先使用屏障,然后使用普通存储,从而防止使用较早的加载/存储进行重新排序,但不会停止使用较晚的进行重新排序。如果seq_cst加载避免在其自身之前需要完全屏障(这是正常情况),则存储可以在加载之后重新排序。

例如,ARMv7上的发布存储是dmb ish; str,而seq_cst加载是ldr; dmb ish,因此您拥有的str/ldr之间没有任何障碍。

在PowerPC上,由于seq_cst负载hwsync; ld; cmp; bc; isync,因此在加载之前有一个完整的屏障。(我认为重量级同步是防止IRIW重新排序的一部分,它阻止同一物理核心上的SMT线程之间的存储转发,只有在其他核心的存储实际上变得全局可见时才能看到它们。)

相关文章