双存储-加载原子操作场景下的内存顺序选择:如何确保线程1读取到b=1或线程2读取到a=1?
避免双0结果的内存顺序选择
嘿,这个问题是并发内存模型里的经典场景,咱们一步步拆解清楚:
首先明确我们要杜绝的坏情况:线程1读取到b=0,同时线程2读取到a=0——也就是所谓的“双0”结果。
为什么不能用memory_order_relaxed?
松弛内存顺序是最宽松的原子操作顺序规则,编译器和CPU可以自由重排同一线程内的原子操作(只要不违反线程内部的执行逻辑)。比如:
- 线程1可能被重排成先读b(拿到0),再写a;
- 线程2可能被重排成先读a(拿到0),再写b。
这种重排直接导致双0结果,而且松弛顺序下没有跨线程的同步关系,一个线程的写操作不一定会被另一个线程及时看到,进一步加剧了这种风险。
可行的两种方案
方案1:顺序一致内存顺序(memory_order_seq_cst)
这是最直观、最可靠的选择,所有标记为memory_order_seq_cst的原子操作会遵循一个全局统一的线性执行顺序——就像整个系统里的所有原子操作都按某一条时间线依次执行。
对这个场景来说,全局顺序里必然会出现以下两种情况之一:
- 线程1的写
a=1操作发生在线程2的读a操作之前:线程2读a时会拿到1; - 线程2的写
b=1操作发生在线程1的读b操作之前:线程1读b时会拿到1。
绝对不会出现“线程1先读b、线程2先读a”都发生在对方写操作之前的情况,因为全局顺序是线性的。这个方案的唯一缺点是性能稍逊(CPU需要做更多同步工作),但胜在简单易懂,适合对性能要求不极致的场景。
方案2:Release-Acquire内存顺序对(memory_order_release + memory_order_acquire)
这是更轻量的高性能方案,具体做法是:
- 线程1的原子写
a=1用memory_order_release; - 线程1的原子读
b用memory_order_acquire; - 线程2的原子写
b=1用memory_order_release; - 线程2的原子读
a用memory_order_acquire。
它能避免双0的核心原因有两点:
- 禁止线程内重排:
release写会阻止任何后续操作被重排到该写操作之前;acquire读会阻止任何之前的操作被重排到该读操作之后。也就是说:- 线程1里,写
a一定发生在读b之前; - 线程2里,写
b一定发生在读a之前。
直接杜绝了“先读再写”的重排导致的双0。
- 线程1里,写
- 建立跨线程同步关系:如果线程2用
acquire读a时拿到了1,那么线程1的release写a就和线程2的acquire读a建立了「同步-先行」关系——线程1写a之前的所有操作(这里就是写a本身)都会被线程2读a之后的操作可见。反过来,如果线程1用acquire读b拿到了1,线程2的release写b也会和线程1的acquire读b建立同步关系。
这种方案在保证正确性的同时,比顺序一致顺序的性能损耗更小,适合对性能敏感的场景。
总结
- 追求简单可靠:选
memory_order_seq_cst; - 追求高性能:选
memory_order_release和memory_order_acquire的配对方案。
内容的提问来源于stack exchange,提问作者uncertainty principle
相关产品推荐
相关产品推荐

