memory_order_seq_cst 和 memory_order_release 的可能排序
Possible orderings with memory_order_seq_cst and 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), then apply asm memory-ordering rules. Applying this reasoning assumes that the normal C++->asm mappings 是安全的,并且总是至少与 C++ 内存模型一样强大)来推理要容易得多。如果您可以通过这种方式找到合法的重新排序,你不需要费力地通过 C++ 形式主义。但如果你不这样做,那当然不能证明它在 C++ 抽象机中是安全的。)
无论如何,实现的关键点是 seq_cst 操作不像 atomic_thread_fence(mo_seq_cst)
- 单个 seq_cst
操作只有recover/maintain 它们与其他 seq_cst
操作交互方式的顺序一致性,而不是普通 acquire/release/acq_rel。 (同样,获取和释放栅栏是更强的双向障碍,不像获取和释放 operations 作为 Jeff 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 上,发布和 seq_cst 商店使用 STLR。 seq_cst 加载使用 LDAR,它与 STLR 有特殊的交互,在最后一个 STLR 从存储缓冲区中耗尽之前不允许读取缓存。所以 ARMv8 上的 release-store / seq_cst load 和 seq_cst store / seq_cst load 是一样的。 (ARMv8.3 添加了 LDAPR,通过让获取加载以不同方式编译来允许真正获取/释放;参见 。)
然而,它也可能发生在许多使用单独屏障指令的 ISA 上,例如 ARM32:发布存储通常会使用屏障完成,然后是普通存储,以防止使用 earlier[= 重新排序64=] 加载/存储,但不停止稍后重新排序。如果 seq_cst 加载避免在自身之前需要一个完整的屏障(这是正常情况),那么商店可以在加载后重新排序。
例如,ARMv7 上的发布存储是 dmb ish; str
,seq_cst 加载是 ldr; dmb ish
,因此您的 str / ldr 之间没有障碍。
在 PowerPC 上,由于 seq_cst 负载是 hwsync; ld; cmp; bc; isync
,所以在负载之前有一个完整的屏障。 (我认为 HeavyWeight Sync 是防止 IRIW 重新排序的一部分,用于阻止同一物理内核上的 SMT 线程之间的存储转发,只有当其他内核真正变得全局可见时才能看到它们的存储。)
参考以下代码
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), then apply asm memory-ordering rules. Applying this reasoning assumes that the normal C++->asm mappings 是安全的,并且总是至少与 C++ 内存模型一样强大)来推理要容易得多。如果您可以通过这种方式找到合法的重新排序,你不需要费力地通过 C++ 形式主义。但如果你不这样做,那当然不能证明它在 C++ 抽象机中是安全的。)
无论如何,实现的关键点是 seq_cst 操作不像 atomic_thread_fence(mo_seq_cst)
- 单个 seq_cst
操作只有recover/maintain 它们与其他 seq_cst
操作交互方式的顺序一致性,而不是普通 acquire/release/acq_rel。 (同样,获取和释放栅栏是更强的双向障碍,不像获取和释放 operations 作为 Jeff 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 上,发布和 seq_cst 商店使用 STLR。 seq_cst 加载使用 LDAR,它与 STLR 有特殊的交互,在最后一个 STLR 从存储缓冲区中耗尽之前不允许读取缓存。所以 ARMv8 上的 release-store / seq_cst load 和 seq_cst store / seq_cst load 是一样的。 (ARMv8.3 添加了 LDAPR,通过让获取加载以不同方式编译来允许真正获取/释放;参见
然而,它也可能发生在许多使用单独屏障指令的 ISA 上,例如 ARM32:发布存储通常会使用屏障完成,然后是普通存储,以防止使用 earlier[= 重新排序64=] 加载/存储,但不停止稍后重新排序。如果 seq_cst 加载避免在自身之前需要一个完整的屏障(这是正常情况),那么商店可以在加载后重新排序。
例如,ARMv7 上的发布存储是 dmb ish; str
,seq_cst 加载是 ldr; dmb ish
,因此您的 str / ldr 之间没有障碍。
在 PowerPC 上,由于 seq_cst 负载是 hwsync; ld; cmp; bc; isync
,所以在负载之前有一个完整的屏障。 (我认为 HeavyWeight Sync 是防止 IRIW 重新排序的一部分,用于阻止同一物理内核上的 SMT 线程之间的存储转发,只有当其他内核真正变得全局可见时才能看到它们的存储。)