为何C++11中的acquire/release无法保证顺序一致性?
关于C++原子内存序中断言失败的困惑解答
先看咱们讨论的这段代码:
// 线程1 y.store(20, memory_order_release); x.store(10, memory_order_release); // 线程2 if (x.load(memory_order_acquire) == 10) { assert(y.load(memory_order_acquire) == 20); y.store(10, memory_order_release); } // 线程3 if (y.load(memory_order_acquire) == 10) { assert(x.load(memory_order_acquire) == 10); }
根据GCC原子操作文档的“Overall Summary”章节,线程3里的assert(x.load(memory_order_acquire) == 10)是有可能触发失败的,你对此的困惑和理解我都get到了,但这里有几个关键的认知误区,咱们慢慢捋清楚:
你的前两个理解点是完全正确的:
- 线程3的acquire屏障确实会阻止LoadLoad重排序,所以它先读y再读x的顺序不会被CPU打乱
- 线程1的release屏障也确实会阻止StoreStore重排序,所以y的写入一定在x的写入之前完成
但后面两点的理解存在偏差,核心问题在于:release-acquire的同步关系是「线程对之间」的绑定,不是全局生效的广播。
咱们一步步拆解为什么断言可能失败:
- 线程1和线程2之间确实有同步关系:线程1的
x.store(10, release)和线程2的x.load(acquire)配对成功后,线程1中所有在x.store之前的操作(也就是y.store(20))对线程2是可见的,所以线程2里的第一个assert永远不会失败——这部分你没说错。 - 但线程2和线程3的同步关系,只绑定了它们俩:线程2的
y.store(10, release)和线程3的y.load(acquire)配对后,只能保证线程2中在y.store之前的操作对线程3可见,这和线程1的操作没有直接关系。 - 那为什么线程3可能看不到x=10?这里要结合CPU的存储缓冲区和缓存一致性来看:
- 线程1的两个release store虽然顺序不会乱,但release只保证「当前线程内的操作顺序对后续配对的acquire可见」,不代表这些写操作会立刻被刷新到全局缓存。CPU可能把
y.store(20)留在自己的存储缓冲区里,先把x.store(10)刷新到缓存。 - 当线程2读到x=10时,因为release-acquire的同步规则,线程1的存储缓冲区里的
y.store(20)会被强制同步到缓存,所以线程2能读到y=20,没问题。然后线程2把y改成10并刷新到缓存。 - 线程3读到y=10后,触发assert,但它和线程1之间没有任何同步关系——缓存一致性协议只保证写操作最终会被所有线程看到,但不保证「线程3看到线程2的y=10」和「线程3看到线程1的x=10」的顺序。哪怕线程1的x=10已经在缓存里,线程3的CPU也可能还没收到缓存失效的通知,或者因为没有同步纽带,CPU允许线程3先读取x的旧值,再处理缓存更新。
- 线程1的两个release store虽然顺序不会乱,但release只保证「当前线程内的操作顺序对后续配对的acquire可见」,不代表这些写操作会立刻被刷新到全局缓存。CPU可能把
简单来说:线程3只和线程2“认亲”,所以只能看到线程2的操作;线程2和线程1“认亲”,但这种关系不会自动传递给线程3。所以线程3完全可能看到线程2写的y=10,却看不到线程1写的x=10,导致断言失败。
内容的提问来源于stack exchange,提问作者Pengcheng
相关产品推荐
相关产品推荐

