关于std::atomic的relaxed内存序下fetch_add与load操作的可见性疑问
嘿,这个问题问得特别准,正好戳中了C++原子操作内存序里最容易绕晕的点!
首先得给你的理解点个赞——你抓对了核心:std::memory_order_relaxed确实不提供跨线程的可见性保证。说白了,这种内存序只保证操作本身是原子的(不会出现半写半读的中间值),但完全不管不同线程之间的可见性。所以你说T2可能一直读到缓存里的旧值、永远看不到T1的fetch_add更新,这在C++标准的框架下是完全合法的——编译器或者硬件完全可以这么干,不违反任何规则。
那是不是必须用std::memory_order_release(T1)加std::memory_order_acquire(T2)?其实这是最常用、开销最小的正确方案,但也不是唯一的选择,先给你掰扯清楚:
先说说release-acquire的作用:当T1用release语义执行
fetch_add,T2用acquire语义执行load时,这俩操作就建立了一个同步关系。按照C++标准的规定,T1里所有在这个release操作之前完成的写操作,对T2里这个acquire load之后的代码都是可见的。放到你的场景里,就是T2的acquire load一定能看到T1的fetch_add更新(只要T1的操作确实在T2的load之前执行过)。除了release-acquire,你也可以用
std::memory_order_seq_cst(顺序一致性语义),给T1的fetch_add和T2的load都加上这个内存序。它会保证所有线程看到的原子操作顺序都是全局一致的,当然也能确保可见性,但代价是性能更高——因为它会限制CPU和编译器的优化空间。至于你提到的“Visible side-effects”,C++标准里明确说了:一个线程的副作用要对另一个线程可见,必须存在对应的同步关系。relaxed操作不属于同步操作,自然没法触发这种可见性保证。
这里还要补充个容易混淆的点:relaxed操作虽然没有可见性保证,但它遵守修改顺序一致性——也就是说,对于同一个原子变量,所有线程看到的修改操作的先后顺序是一致的。但这和可见性是两码事:比如T1改了值,T2可能一直看不到,但如果某天T2突然看到了T1的修改,那之后它就不能再看到T1修改之前的旧值了。
总结一下:你的核心判断是对的——relaxed内存序下没有跨线程可见性保证,要确保T2能看到T1的更新,必须用带同步语义的内存序,release-acquire是最推荐的方案。
内容来源于stack exchange

