连续原子操作的Acquire-Release内存模型相关技术疑问
以下代码在一个线程中写入原子变量A、B,在另一个线程中以相反顺序读取它们:
#include <atomic> #include <thread> // Initially. std::atomic<int> A = std::atomic<int>(0); std::atomic<int> B = std::atomic<int>(0); // Thread 1 void write() { A.store(1, std::memory_order_release); // line 1 B.store(1, std::memory_order_release); // line 2 } // Thread 2 void read() { int valB = B.load(std::memory_order_acquire); // line 3 int valA = A.load(std::memory_order_acquire); // line 4 } int main() { std::thread t1(write), t2(read); t1.join(); t2.join(); return 0; }
理想情况下,考虑到原子变量B的release-acquire顺序,线程2中第3行的load操作应与线程1中第1行的store操作完成同步,因为第1行操作发生在第2行的release操作之前。但参考文档提到(重点为提问者所加):
从线程A的视角来看,所有发生在原子store操作之前的内存写入**(非原子操作和relaxed原子操作)**,都会成为线程B中可见的副作用。
那么,针对B的release-acquire顺序是否不会影响其他非relaxed内存顺序的原子操作?是否可能在线程2的read()执行结束后出现valB = 1、valA = 0的情况?因为B的内存顺序不关心其他非relaxed的原子操作?我们是否需要使用sequential consistency?
核心结论
是的,完全可能出现valB=1且valA=0的情况,这是C++内存模型允许的合法行为。Release-Acquire同步在这里并没有保证A的store操作对线程2可见,具体原因如下:
1. Release-Acquire的作用边界
你引用的文档是简化表述,完整的C++内存模型中,release操作的"之前的写入"包含当前线程中所有happens-before该release操作的内存操作——无论操作是原子还是非原子,也不管原子操作的内存顺序。但这里的关键问题是:线程1中A的store(line1)和B的store(line2)之间没有强制的顺序约束。
在单线程内部,代码顺序是先写A再写B,但memory_order_release并不禁止CPU对这两个store操作做重排序(只要不违反线程内的happens-before规则)。也就是说,从其他线程的视角看,B的store完全可能先于A的store完成。
2. 为什么Release-Acquire没生效
当线程2通过acquire load读到B=1时,它只能保证:线程1中所有happens-before B的release store的操作,对线程2可见。但如果线程1中A的store被重排到了B的store之后,那么A的store并不在这个"happens-before"集合里,自然无法保证对线程2可见。
3. 如何确保预期行为
如果想保证线程2读到B=1时,一定能读到A=1,有两种可靠方案:
- 方案一:使用顺序一致性(sequential consistency)
将所有原子操作的内存顺序改为std::memory_order_seq_cst,这种顺序会强制所有原子操作的全局总顺序,禁止任何破坏程序逻辑顺序的重排序。但它的性能开销是所有内存顺序中最大的。// Thread1 A.store(1, std::memory_order_seq_cst); B.store(1, std::memory_order_seq_cst); // Thread2 int valB = B.load(std::memory_order_seq_cst); int valA = A.load(std::memory_order_seq_cst); - 方案二:显式建立A和B的happens-before关系
不需要全局顺序,只要确保线程1中A的store一定happens-before B的store。可以通过插入内存屏障,或者让B的store依赖于A的操作结果来实现:
或者通过依赖关系禁止重排:void write() { A.store(1, std::memory_order_release); std::atomic_thread_fence(std::memory_order_release); // 强制之前的操作完成后再执行后续store B.store(1, std::memory_order_release); }void write() { A.store(1, std::memory_order_relaxed); // 通过A的load结果作为B的store值,建立依赖关系,禁止重排 B.store(A.load(std::memory_order_relaxed), std::memory_order_release); }
4. 总结
Release-Acquire同步只保证"release操作之前已完成的操作"对acquire线程可见,但它不保证release操作之前的原子操作不会被重排到release操作之后。如果需要跨线程的操作顺序保证,要么使用顺序一致性,要么显式建立操作之间的happens-before关系。
内容的提问来源于stack exchange,提问作者Akash

