memory_order_relaxed
是 C/C++ 原子操作中最宽松的内存顺序,允许编译器和 CPU 对指令进行重排序(包括编译器优化和硬件层面的乱序执行)。这种重排序的目的是提高性能,但也会导致多线程程序中的行为变得复杂。
1. 允许的重排序类型
memory_order_relaxed
仅保证操作的原子性(即操作的完整性,不会被部分观察到),但不保证操作的顺序性和可见性。具体来说:
(1) 编译器重排序
编译器可能会根据优化策略,重新排列代码中的指令顺序。例如:
c
atomic<int> x(0), y(0);
// 线程 1
x.store(1, memory_order_relaxed); // A
y.store(1, memory_order_relaxed); // B
// 编译器可能将 B 重排到 A 之前执行!
(2) CPU 硬件重排序
现代 CPU 会通过乱序执行(Out-of-Order Execution)优化性能。例如:
- Store-Store 重排序:两个写入操作可能被重排。
- Load-Load 重排序:两个读取操作可能被重排。
- Load-Store 重排序:读取和写入操作可能被重排。
对于 memory_order_relaxed
,这些重排序均可能发生。
2. 重排序的影响
示例 1:多线程可见性问题
c
atomic<int> x(0), y(0);
// 线程 1
x.store(1, memory_order_relaxed); // 操作 A
y.store(1, memory_order_relaxed); // 操作 B
// 线程 2
while (y.load(memory_order_relaxed) != 1) {} // 操作 C
assert(x.load(memory_order_relaxed) == 1); // 操作 D:可能失败!
- 由于允许 Store-Store 重排序,线程 1 可能先执行 B 再执行 A。
- 如果线程 2 观察到
y == 1
但x == 0
,断言将失败。
示例 2:无依赖关系的操作
c
atomic<int> a(0), b(0);
// 线程 1
a.store(1, memory_order_relaxed); // 操作 1
b.store(2, memory_order_relaxed); // 操作 2
// 其他线程可能观察到操作 2 先于操作 1!
3. 什么情况下不会被重排序?
即使使用 memory_order_relaxed
,以下规则仍成立:
- 单线程依赖顺序:同一线程内,如果操作 B 依赖于操作 A 的结果,编译器/CPU 不会重排 A 和 B。
c
int tmp = a.load(memory_order_relaxed); // A b.store(tmp, memory_order_relaxed); // B:依赖 A 的结果,不会被重排
- 同一原子变量的修改顺序:对同一原子变量的修改,所有线程会观察到一致的修改顺序。
4. 何时使用 memory_order_relaxed
?
适用场景:
- 不需要同步的原子操作(例如统计计数器)。
- 操作顺序无关紧要(例如多个独立的标志位)。
- 需要结合其他内存顺序(如
acquire-release
)手动构建同步逻辑。
示例:无锁计数器
c
atomic<int> counter(0);
// 多个线程并发累加
void increment() {
counter.fetch_add(1, memory_order_relaxed);
}
- 计数器的累加无需同步其他内存操作,使用
relaxed
可以提升性能。
5. 总结
特性 | memory_order_relaxed |
---|---|
原子性 | ✔️ 保证原子操作(不会被部分观察到) |
顺序性 | ❌ 允许编译器/CPU 重排序 |
可见性 | ❌ 不保证其他线程立即看到操作结果 |
适用场景 | 不需要同步的独立原子操作(如计数器、标志位) |
性能 | 最高(无同步开销) |
注意事项
- 在需要跨线程同步(如传递数据)时,必须使用更强的内存顺序(如
acquire-release
或seq_cst
)。 - 滥用
relaxed
可能导致极难调试的内存一致性问题。