一、内存序是什么?先别急着背定义
内存序(memory order)这玩意儿,说白了就是告诉编译器和CPU:你对内存操作的顺序,能容忍多大程度的乱序。
我刚开始学C++11的内存模型时,觉得这东西离我挺远的。直到有一次,我在一个多生产者-单消费者的日志系统里,遇到了诡异的数据丢失——明明代码逻辑看起来完全正确,但跑在ARM芯片上就是偶尔丢数据。嗯,从那以后,我再也不敢轻视内存序了。
你想想看,现代CPU和编译器为了性能,会偷偷重排你的指令。单线程下没问题,但多线程下,这种「好心」就会酿成大祸。C++11引入的6种内存序,就是用来约束这种「好心」的。
二、六种内存序全景图
先给你一张总览图,心里有个谱:
三、逐个拆解,一个都别想跑
1. memory_order_relaxed —— 最自由的野马
relaxed 是最弱的内存序。它只保证原子操作本身的原子性,不保证任何顺序约束。说白了,就是「我只管这次读写是原子的,至于它和别的操作谁先谁后,我不管」。
我在项目中用 relaxed 最多的地方就是统计计数器。比如记录某个函数被调用了多少次,或者统计内存分配的总字节数——这些场景只关心最终值,不关心顺序。
// 统计计数器示例
std::atomic<uint64_t> total_allocated{0};
void* my_alloc(size_t size) {
// relaxed 足够,我们只关心总数,不关心顺序
total_allocated.fetch_add(size, std::memory_order_relaxed);
return malloc(size);
}
2. memory_order_consume —— 被遗忘的角落
consume 是个很有意思的存在。它只保证数据依赖的有序性。什么意思?就是如果 B 依赖于 A 的值,那么 A 的写入一定在 B 读取之前完成。
但说实话,我几乎不用 consume。为什么?因为大多数编译器(包括 GCC 和 Clang)都把 consume 直接升级成了 acquire。C++17 甚至明确建议「不要用 consume,用 acquire 代替」。
3. memory_order_acquire —— 读操作的守护者
acquire 是一个读操作屏障。它保证:在这个 load 操作之后的所有读和写,都不会被重排到这个 load 之前。
你想想看,这有什么用?最常见的场景就是检查标志位:
std::atomic<bool> ready{false};
std::string data;
// 线程1:生产者
void producer() {
data = "Hello, World!"; // 1. 写数据
ready.store(true, std::memory_order_release); // 2. 设置标志
}
// 线程2:消费者
void consumer() {
while (!ready.load(std::memory_order_acquire)) { // 3. 检查标志
// 等待
}
std::cout << data << std::endl; // 4. 读数据 —— 保证能看到线程1写入的值
}
这里 acquire 保证了:第4步的读数据,一定不会重排到第3步之前。所以一旦我们看到 ready 为 true,就一定能读到正确的 data。
4. memory_order_release —— 写操作的守护者
release 和 acquire 是天生一对。它是写操作屏障,保证:在这个 store 操作之前的所有读写,都不会被重排到这个 store 之后。
还是上面那个例子,release 保证了:第1步写 data 一定在第2步写 ready 之前完成。这样消费者看到 ready 为 true 时,data 一定已经写好了。
5. memory_order_acq_rel —— 读写通吃
acq_rel 就是 acquire + release 的合体。它同时具备读屏障和写屏障的效果。通常用在读-改-写(RMW)操作上,比如 fetch_add、compare_exchange 等。
std::atomic<int> counter{0};
// 一个典型的 RMW 操作
int expected = counter.load(std::memory_order_relaxed);
while (!counter.compare_exchange_weak(
expected, expected + 1,
std::memory_order_acq_rel, // 成功时用 acq_rel
std::memory_order_relaxed // 失败时用 relaxed
)) {
// 重试
}
为什么 RMW 操作需要 acq_rel?因为你要同时保证:读取旧值时能看到别人的写入(acquire),写入新值时别人也能看到你的写入(release)。
6. memory_order_seq_cst —— 最安全的默认选择
seq_cst 是最强的内存序,也是 std::atomic 的默认值。它保证所有线程看到的操作顺序完全一致——就像所有操作都在一个全局时钟下顺序执行一样。
但代价呢?性能开销最大。在 x86 上还好,因为 x86 本身的内存模型就比较强。但在 ARM 或 PowerPC 上,seq_cst 会插入昂贵的内存屏障指令。
四、一张表总结所有内存序
| 内存序 | 约束强度 | 读屏障 | 写屏障 | 全局一致 | 典型场景 |
|---|---|---|---|---|---|
| relaxed | 最弱 | ❌ | ❌ | ❌ | 计数器、统计 |
| consume | 弱 | 数据依赖 | ❌ | ❌ | 指针解引用(不推荐) |
| acquire | 中 | ✅ | ❌ | ❌ | 读标志位、加载 |
| release | 中 | ❌ | ✅ | ❌ | 写标志位、存储 |
| acq_rel | 强 | ✅ | ✅ | ❌ | RMW 操作 |
| seq_cst | 最强 | ✅ | ✅ | ✅ | 默认选择、复杂同步 |
五、避坑指南:我踩过的那些坑
坑1:以为 relaxed 能保证顺序
我曾经写过一个无锁队列,用 relaxed 来操作 head 和 tail 指针。结果在 ARM 上跑,消费者读到了半写的数据。后来加上 acquire/release 才修好。记住:relaxed 只保证原子性,不保证顺序。
坑2:acquire 和 release 不配对
有个同事写代码,生产者用 release 写标志,消费者用 relaxed 读标志。结果消费者永远看不到标志变化——因为 relaxed 读不会建立任何 happens-before 关系。acquire 和 release 必须成对出现。
坑3:过度使用 seq_cst
我见过一个项目,所有原子操作都用 seq_cst。性能测试发现瓶颈全在内存屏障上。后来把大部分改成 acquire/release,性能提升了 3 倍。seq_cst 虽好,但别滥用。
六、总结:怎么选?
给你一个简单的决策树:
- 只需要原子性,不关心顺序? → relaxed(计数器、统计)
- 需要同步数据,一个写一个读? → release + acquire 配对
- 需要读-改-写操作? → acq_rel
- 不确定选哪个? → seq_cst(默认值,最安全)
- 想用 consume? → 别想了,用 acquire 代替
最后说一句:内存序这东西,光看文档是学不会的。我建议你写几个小 demo,在不同的架构上跑一跑,亲眼看看不同内存序的行为差异。实践出真知,这话一点不假。
公众号:蓝海资料掘金营,微信deep3321