真实硬件中原子变量更新值经间接路径比直接路径更早可见的可能性问询
这是个非常有意思的问题,核心就是想问:在真实硬件上,有没有可能一个原子变量的更新值,通过第三方线程转发的间接路径,比直接读取原变量的路径更早被另一个线程看到?放到你给出的代码场景里,就是thread3会不会先读到y的新值(由thread2转发的x的值),但还没读到x的新值?
先把你整理的C++代码贴出来方便讨论:
#define general_barrier std::atomic_thread_fence(memory_order_seq_cst) #define read_barrier std::atomic_thread_fence(memory_order_acquire) // 初始值都是0 std::atomic<int> x, y; void thread1() { x.store(1, std::memory_order_relaxed); // W0:写x=1 // 你这里的注释是对的,单个操作不存在重排问题 } void thread2() { int r2 = x.load(std::memory_order_relaxed); // R0:读x的值 general_barrier; // F1:你质疑这个全内存屏障是否必要 y.store(r2, std::memory_order_relaxed); // W1:把读到的x的值写入y } void thread3() { int r3_a = y.load(std::memory_order_relaxed); // R1:读y(间接路径) read_barrier; // F2:确保读y在x之前 int r3_b = x.load(std::memory_order_relaxed); // R2:读x(直接路径) if( r3_a == 1 ){ assert(r3_b == 1); // 会不会断言失败? } }
一、加上general_barrier的场景
按照Linux文档的描述,加上这个seq_cst全内存屏障后,断言失败的情况是不可能发生的:
- 屏障F1强制thread2的读x操作全局可见地完成后,才会执行写y操作;
- thread3的读屏障(acquire)确保它先完成读y,再执行读x;
- 加上
seq_cst栅栏的全局同步特性,所有线程对这些内存操作的顺序会达成一致。所以如果thread3读到y=1,那x=1肯定已经对它可见了。
二、你对thread2中栅栏必要性的疑问
你觉得thread2里的栅栏多余,因为读x和写y之间有数据依赖——写y必须用到读x得到的r2,所以硬件不可能把写y重排到读x前面。这个理解在单个线程内的本地执行顺序上是完全正确的,但忽略了跨CPU的全局内存可见性问题,尤其是在非多拷贝原子(non-multicopy-atomic)的系统上。
三、去掉general_barrier的场景
Linux文档明确指出,这种情况下,在非多拷贝原子的真实硬件上,完全可能出现thread3读到y=1但x=0的情况,原因是:
如果thread1和thread2共享缓存层级或存储缓冲区(比如在同一个CPU socket上),thread2可能比thread3更早看到thread1写的x=1——比如通过共享的L1缓存直接获取,不需要等待全局缓存同步。这时候thread2写y=1的操作,可能通过自身的缓存快速同步到thread3,但thread1写的x=1还没通过全局缓存一致性协议同步到thread3所在的核心。
四、关于你提出的“forwarding-atomicity”
你期望的“中转路径不能比直接路径快”的特性,其实是多拷贝原子系统的固有属性——这类系统要求一个写操作对所有CPU的可见性是原子的:要么所有CPU都看到新值,要么都看到旧值。但真实硬件中确实存在非多拷贝原子的情况(比如部分ARM/PowerPC架构的实现,或者NUMA系统中跨节点的内存访问),这类系统中写操作的可见性是逐步扩散的,先到相邻核心/缓存,再扩散到其他节点,这就给“中转路径更快”提供了可能。
结论
在真实的非多拷贝原子硬件上,去掉thread2的全内存屏障后,确实可能出现y的新值比x的新值更早被thread3看到;而加上seq_cst全屏障后,这种情况会被禁止,因为全屏障会强制刷新存储缓冲区,确保所有核心看到的操作顺序一致。
内容来源于stack exchange

