在同内存序的atomic load/store前使用std::atomic_thread_fence是否总是冗余?
结论
移除这个std::atomic_thread_fence调用确实会产生可观测的差异,二者语义并不等价。
核心原因
C++内存模型中,带std::memory_order_acquire语义的原子加载,同步范围仅绑定到当前加载的原子变量:只有当这次加载读到了对应变量的release写入值时,才能保证本次加载之后的内存访问能看到该release写入之前的所有操作。
而前置的std::atomic_thread_fence(std::memory_order_acquire)的同步范围覆盖所有fence之后的内存操作:只要是fence之后的任意读操作读到了任意release操作(或release fence之后的写入)的值,都能和对应的release侧建立同步,看到release之前的所有写操作。
可复现差异的示例
#include <atomic> #include <cassert> // 全局变量 std::atomic<uint64_t> b{0}; std::atomic<int> c{0}; int data{0}; // 普通非原子共享变量 // 写入线程执行的逻辑 void writer_thread() { data = 123; // 先修改普通变量 c.store(1, std::memory_order_release); // 对c的release写入 b.store(1, std::memory_order_release); // 对b的release写入 } // 原始带fence的f函数 void f_with_fence() { std::atomic_thread_fence(std::memory_order_acquire); uint64_t a = b.load(std::memory_order_acquire); int c_val = c.load(std::memory_order_relaxed); // c的加载使用relaxed语义 if (a == 1 && c_val == 1) { // 有acquire fence保证:只要fence之后读到了c的release写入值1,就能看到writer_thread中c.store之前的所有写 assert(data == 123); // 这个断言永远不会触发 } } // 移除fence后的f函数 void f_without_fence() { uint64_t a = b.load(std::memory_order_acquire); int c_val = c.load(std::memory_order_relaxed); // c的加载仍为relaxed语义 if (a == 1 && c_val == 1) { // 只有b的acquire加载的同步保证,仅能保证看到b.store之前的写,c的relaxed加载没有同步保证 // 即使c_val读到了1,也可能看不到data的修改 assert(data == 123); // 这个断言在弱内存模型架构上存在触发可能 } }
差异说明
在ARM、PowerPC这类弱内存模型的硬件平台上,移除fence后的版本完全可能出现a=1、c_val=1但data仍然为0的异常情况。因为c的relaxed加载没有和写入线程的c.release写建立同步关系,而原始的acquire fence可以覆盖所有fence之后的读操作,自动和匹配的release操作建立同步。
内容的提问来源于stack exchange,提问作者Joseph Garvin
相关产品推荐
相关产品推荐

