C++ 内存模型:从原子操作到 happens-before 关系
为什么需要内存模型
在没有内存模型的世界里,编译器重排、CPU 乱序执行会让多线程程序的行为变得不可预测。C++11 引入的内存模型,本质上是程序员与编译器/CPU 之间的一份契约:在哪些点上,哪些操作可见性必须被保证。
六种内存序
#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> data{0};
std::atomic<bool> ready{false};
void producer() {
data.store(42, std::memory_order_relaxed); // (1)
ready.store(true, std::memory_order_release); // (2)
}
void consumer() {
while (!ready.load(std::memory_order_acquire)) // (3)
;
std::cout << data.load(std::memory_order_relaxed) << "\n"; // (4) 一定看到 42
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join(); t2.join();
}
上面这个例子是 acquire-release 配对的最经典场景。理解它的关键不是记 API,而是看懂下面这张关系图。
| 内存序 | 语义 | 典型用途 |
|---|---|---|
relaxed |
只保证原子,不保证顺序 | 计数器、统计 |
consume |
acquire 的数据依赖版本(实际退化为 acquire) | 极少使用 |
acquire |
load 之后的读写不能重排到 load 前 | 读锁、接收同步 |
release |
store 之前的读写不能重排到 store 后 | 写锁、发布数据 |
acq_rel |
同时具备 acquire 与 release | RMW 操作(如 exchange) |
seq_cst |
全局总顺序,最强保证 | 默认值,性能最低 |
happens-before 是核心
内存模型的所有结论,都可以归约到 happens-before 这一个关系上。它的传递性构成了线程间的同步链:
- 同一线程内:程序顺序
A; B;⇒A happens-before B(sequenced-before)。 - 跨线程:若 release-store 被 acquire-load 观察到,则 store 之前的所有操作 happens-before load 之后的所有操作。
- 传递性:
A happens-before B且B happens-before C⇒A happens-before C。
回到上面的例子:(1) sequenced-before (2),(2) 同步于 (3)(因为 (3) 读到了 (2) 写入的 true),(3) sequenced-before (4)。由传递性:(1) happens-before (4),所以 (4) 一定读到 42。
一个常见的反例:用 relaxed 做同步
std::atomic<int> flag{0};
// 线程 A
data = 42; // 普通写
flag.store(1, std::memory_order_relaxed); // ❌ 不能保证 data 对线程 B 可见
// 线程 B
if (flag.load(std::memory_order_relaxed)) { // ❌ 即使读到 1,也不保证看到 data=42
use(data);
}
relaxed 只保证 flag 自身的原子性,不会建立任何跨线程的 happens-before 关系。编译器/CPU 完全可能把 data = 42 重排到 flag.store 之后,于是线程 B 看到了 flag=1 却读到 data 的旧值。
seq_cst 的代价
seq_cst(顺序一致)是最强保证,它要求所有 seq_cst 操作存在一个全局总顺序,所有线程都看到同一个顺序。代价是性能:在 x86 上 store 需要插入 mfence 或用 xchg,在 ARM 上需要 dmb ish。
// 默认是 seq_cst
std::atomic<int> x{0};
x.store(1); // 等价于 x.store(1, std::memory_order_seq_cst);
经验法则:先用 seq_cst 保证正确,再用 profiler 找到热点降级为 acq_rel。不要一开始就追求极致性能而用 relaxed,那是 bug 的温床。
小结
- 记住 happens-before 的三条规则,比背六种内存序更有用。
- acquire/release 是性价比最高的同步原语,覆盖 90% 的场景。
- relaxed 只用于"我不关心顺序,只关心原子性"的场景,如自增计数。