为何relaxed操作可重排序?程序序与happens-before的关系及重排条件
在《C++ Concurrency in Action》中,作者提到relaxed ordering的规则:
不同变量上的relaxed操作可自由重排序,只要它们遵循所绑定的任何happens-before关系
而cppreference的memory_order页面给出了如下示例:
// Thread 1: r1 = y.load(std::memory_order_relaxed); // A x.store(r1, std::memory_order_relaxed); // B // Thread 2: r2 = x.load(std::memory_order_relaxed); // C y.store(42, std::memory_order_relaxed); // D
该示例允许出现
r1 == r2 == 42的结果,因为尽管线程1中A被sequenced-before B,线程2中C被sequenced-before D,但没有任何规则阻止D在y的修改顺序中出现在A之前,也没有规则阻止B在x的修改顺序中出现在C之前。线程1中的加载操作A可能看到线程2中D对y的副作用,线程2中的加载操作C可能看到线程1中B对x的副作用。这种情况尤其可能在D在线程2中先于C完成时发生,无论是编译器重排序还是运行时重排序导致的。
我的疑问是:
- 线程2中C被sequenced-before D,是否意味着C也happens-before D?
- 操作D和C的重排序是否与书中“relaxed操作可重排序但必须遵循happens-before关系”的说法矛盾?
- relaxed操作在什么条件下可以重排序?
解答
1. Sequenced-before 必然等价于同一个线程内的happens-before
是的,C++标准明确规定:同一线程内,如果操作X被sequenced-before操作Y,那么X一定happens-before Y。所以线程2里的C确实happens-before D。
2. 重排序和happens-before规则完全不矛盾
这里的核心误区是:happens-before约束的是操作结果的可见性和逻辑一致性,不是操作的实际执行顺序。
书中说的“遵循happens-before关系”,指的是不能破坏线程内的依赖逻辑——比如示例里B用到了A加载的r1,这种数据依赖下,编译器和CPU绝对不能把B重排到A前面,否则B会拿到无效值。但C和D是完全独立的操作:C读x,D写y,互相没有数据依赖,所以即使它们有sequenced-before(也就是happens-before)关系,也允许被调整执行顺序。
换句话说,happens-before不要求操作必须按代码顺序执行,只要求如果X happens-before Y,那么X的结果对Y是可见的(如果Y依赖X的话),同时程序的最终结果要符合至少一种合法的执行序列。示例里r1==r2==42的结果,完全符合这样的合法序列:线程2先执行D,线程1执行A读到42,线程1执行B把42写入x,最后线程2执行C读到42——这个序列完全遵守所有happens-before规则,所以是合法的。
3. Relaxed操作的重排序条件
Relaxed操作的重排序限制极少,只要满足两个核心条件:
- 不破坏线程内的数据依赖:如果操作Y必须用到操作X的结果,那么X和Y的顺序不能被颠倒。
- 遵循单个变量的全局修改顺序:针对同一个变量的所有操作,必须存在一个全局一致的修改顺序,所有线程看到的该变量的变化顺序必须和这个全局顺序匹配。但不同变量的修改顺序之间没有约束,所以不同变量的relaxed操作可以任意交叉重排。
简单总结:只要操作之间无数据依赖,且不违反单个变量的修改顺序一致性,relaxed操作就可以被编译器或CPU自由重排序。
内容的提问来源于stack exchange,提问作者codesavesworld

