使用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);
这里是否可能one
和two
都是0?
(我似乎遇到了一个错误,如果one
和two
在上面的代码运行后都能保持值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
结果。
我将one
和two
重命名为r1
和r2
(每个线程中的本地寄存器和寄存器),以避免写入类似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线程之间的存储转发,只有在其他核心的存储实际上变得全局可见时才能看到它们。)
相关文章