C++ 六种 memory_order
- [C++ 六种 memory_order:从"原子性"到线程间可见性](#C++ 六种 memory_order:从“原子性”到线程间可见性)
-
- [一、atomic 只保护变量是原子的](#一、atomic 只保护变量是原子的)
- [二、怎样让 stage == 1 真正代表结果已经就绪](#二、怎样让 stage == 1 真正代表结果已经就绪)
- [三、六种 memory_order 分别怎么用](#三、六种 memory_order 分别怎么用)
-
- [3.1 memory_order_relaxed:只保证原子变量本身](#3.1 memory_order_relaxed:只保证原子变量本身)
- [3.2 memory_order_release:发布此前完成的写入](#3.2 memory_order_release:发布此前完成的写入)
- [3.3 memory_order_acquire:接收已经发布的数据](#3.3 memory_order_acquire:接收已经发布的数据)
- [3.4 memory_order_acq_rel:既接收数据,又发布数据](#3.4 memory_order_acq_rel:既接收数据,又发布数据)
- [3.5 memory_order_seq_cst:再加一条全局一致顺序](#3.5 memory_order_seq_cst:再加一条全局一致顺序)
- [3.6 memory_order_consume:工程中通常不再使用](#3.6 memory_order_consume:工程中通常不再使用)
- 五、实际写代码时应该怎么选
- [六、memory_order 和内存屏障是什么关系](#六、memory_order 和内存屏障是什么关系)
C++ 六种 memory_order:从"原子性"到线程间可见性
先来看一个再普通不过的问题。
两个线程同时给同一个整数加一,最后的结果一定是 2 吗?
cpp
int count = 0;
// 线程 A // 线程 B
++count; ++count;
答案是不一定。
++count 看起来只有一行,背后却包含"读取旧值、加一、写回新值"三个步骤。两个线程可能同时读到 0,然后分别写回 1。更准确地说,多个线程在没有同步的情况下读写 count 会产生数据竞争,程序行为未定义。
把它改成原子变量以后,问题就解决了:
cpp
std::atomic<int> count{0};
count.fetch_add(1);
无论多少线程同时执行 fetch_add,这次"读取---修改---写回"都不会被其他线程从中间打断。
看到这里,一个新的问题就出现了:既然 atomic 已经保证操作不会被打断,为什么它还要提供下面这个参数?
cpp
count.fetch_add(1, std::memory_order_relaxed);
为什么 C++ 还设计了 relaxed、acquire、release、acq_rel、seq_cst 和 consume 六种不同的内存序?
要回答这个问题,得先看看 atomic 没有解决什么。
一、atomic 只保护变量是原子的
计数器只关心 count 自己,所以保证自增不丢失就够了。但真实程序经常会用一个原子变量表示执行阶段,再根据这个阶段读取其他数据。
例如,线程 A 先把计算结果改成 100,再把阶段改成 1;线程 B 看到阶段 1 后输出结果:
cpp
int result = 0;
std::atomic<int> stage{0};
// 线程 A
result = 100;
stage.store(1, std::memory_order_relaxed);
// 线程 B
if (stage.load(std::memory_order_relaxed) == 1)
std::cout << result;
现在请先不要往下看,想一想:
线程 B 既然已经看到了
stage == 1,它输出的result一定是100吗?
很多人的第一反应是"一定"。因为在线程 A 的代码里,result = 100 明明写在 stage = 1 前面。
但 C++ 给出的答案是:不能这样推断。
stage 的读写确实是原子的,它不会被读坏。但是 relaxed 只保护 stage 自己,没有在线程 A 写 result 和线程 B 读 result 之间建立同步。对普通变量 result 的这组访问存在数据竞争,程序行为未定义。
这并不是说程序"一定会输出旧值 0"。未定义行为意味着任何结果都不能依赖。根本原因在于:线程 A 中的源代码顺序,只能说明这两行在当前线程里的关系,并没有规定线程 B 必须按照相同顺序观察它们。编译器可能重排指令,CPU 也可能让不同写入以不同时间对其他核心可见。
原来,正确的并发程序需要同时解决三件事:
- 原子性:一次操作不能做到一半就被另一个线程插入;
- 有序性:编译器和 CPU 的重排不能破坏程序依赖的先后关系;
- 可见性:一个线程完成的写入,要能被另一个线程安全地观察到。
atomic 首先解决原子性,memory_order 则进一步描述这次原子操作如何约束前后的内存访问,以及它能否在线程之间建立同步。
C++ 一共提供六种内存序:

它们并不是简单的六档"强弱开关"。acquire 负责接收数据,release 负责发布数据,二者的方向就不同。选择哪一种,取决于这个原子操作在多线程协作中承担什么职责。
那么,怎样才能让 stage == 1 真正代表"结果已经准备好了"?
二、怎样让 stage == 1 真正代表结果已经就绪
我们需要在线程 A 和线程 B 之间完成一次数据交接:
cpp
// 线程 A:发布结果
result = 100;
stage.store(1, std::memory_order_release);
// 线程 B:接收结果
if (stage.load(std::memory_order_acquire) == 1)
std::cout << result;
release 表示:在发布 stage = 1 之前,本线程前面的写入已经准备好一起交给其他线程。
acquire 表示:当我读到 stage == 1 后,可以接收发布这个值的线程在此前完成的写入。

当 acquire load 读取到 release store 写入的 1 时,完整关系是:
text
result = 100
↓
release store
↓ 跨线程同步
acquire load
↓
读取 result
于是,线程 A 的 result = 100 happens-before 线程 B 对 result 的读取。此时线程 B 看到的不只是一个阶段数字,还包括线程 A 在发布这个数字之前完成的修改。
这就是 memory_order 真正解决的问题:一个原子变量不仅可以保存自己的值,还可以成为其他数据跨线程传递的交接点。
不过,release 和 acquire 并不是只要同时出现就能自动配对。acquire 必须读取到相应 release 发布的值,这次交接才真正发生。
三、六种 memory_order 分别怎么用
3.1 memory_order_relaxed:只保证原子变量本身
relaxed 是最弱的内存序,但"最弱"不等于"不安全"。它仍然保证原子性,也保证同一个原子对象的修改存在统一的先后顺序。
它不负责同步周围的普通数据。
最典型的场景是统计计数:
cpp
std::atomic<int> requests{0};
requests.fetch_add(1, std::memory_order_relaxed);
我们只希望每次加一都不丢失,并不根据 requests 的值判断其他数据是否已经准备完成。因此只要原子性就够了。
适合使用 relaxed 的场景包括:
- 请求次数、命中次数等统计数据;
- 不承担同步职责的引用计数;
- 唯一编号或事件序号的生成;
- 只关心原子对象自身最终结果的操作。
如果一个原子变量还是其他数据的"就绪信号",通常就不能只使用 relaxed。
3.2 memory_order_release:发布此前完成的写入
release 用在发布数据的一端。
cpp
result = 100;
stage.store(1, std::memory_order_release);
这里的含义是:当其他线程通过 acquire 读取到 stage == 1 时,也应该能够看到 result = 100。
常见场景包括:
- 生产者写入就绪标志;
- 发布已经完成初始化的对象;
- 把任务或节点交给另一个线程;
- 释放锁。锁内部通常也需要类似 release 的语义。
release 可以用于原子 store 和读---改---写操作,不能用于普通原子 load,因为 load 没有新值可以发布。
3.3 memory_order_acquire:接收已经发布的数据
acquire 用在接收数据的一端。
cpp
if (stage.load(std::memory_order_acquire) == 1)
use(result);
如果这次 load 读取到了 release 发布的值,那么发布之前的写入就对当前线程可见。
常见场景包括:
- 消费者读取就绪标志;
- 读取另一个线程发布的对象指针;
- 获取锁;
- 读取某个阶段已经完成的状态。
acquire 可以用于原子 load 和读---改---写操作,不能用于普通原子 store。
release 和 acquire 经常配合使用,可以把它们理解成一次交接:release 负责把数据交出去,acquire 负责把数据接进来。
3.4 memory_order_acq_rel:既接收数据,又发布数据
有些原子操作会先读取旧值,再写入新值,例如 exchange、fetch_add 和 CAS。这类操作统称为读---改---写操作。
如果它既要接收上一个线程发布的数据,又要把当前线程的数据发布给下一个线程,就需要 acq_rel:
cpp
state.compare_exchange_strong(
expected, next, std::memory_order_acq_rel);
CAS 成功时:
- acquire 部分负责获取旧状态关联的数据;
- release 部分负责发布新状态以及此前完成的写入。
常见场景包括:
- 无锁数据结构中的状态更新;
- 多个线程依次推进任务阶段;
- 同时承担"接收上一棒"和"交出下一棒"的 CAS 或
exchange。
acq_rel 只能用于读---改---写操作,不能直接用于普通 load 或 store。
3.5 memory_order_seq_cst:再加一条全局一致顺序
seq_cst 是 sequentially consistent 的缩写,中文通常称为顺序一致性。
它包含 acquire/release 的同步能力,还要求所有 seq_cst 原子操作能够放进一条全局一致的顺序中。不同线程可能不知道真实的执行时间,但它们必须对这些原子操作的先后形成一致认识。
更重要的是,seq_cst 是 C++ 原子操作的默认内存序:
cpp
stage.store(1); // 默认 seq_cst
int value = stage.load();
适合使用 seq_cst 的场景包括:
- 并发代码的第一个正确版本;
- 同时涉及多个原子变量的复杂协议;
- 正确性和可维护性比极限性能更重要的代码;
- 还不能证明较弱内存序一定正确的场景。
它可能比弱内存序限制更多优化,但实际代价与 CPU 架构和操作类型有关,不能简单地认为 seq_cst 一定很慢。
在没有性能数据之前,不要为了"看起来更底层"而主动削弱内存序。
3.6 memory_order_consume:工程中通常不再使用
consume 原本想提供一种比 acquire 更弱的保证:只约束依赖于原子加载结果的后续操作。
cpp
Node* node = head.load(std::memory_order_consume);
use(node->value);
这里对 node->value 的访问依赖于先读取到的 node。理论上,编译器只需要维护这条数据依赖链,不必约束所有后续操作。
问题在于,编译器很难在复杂优化中可靠追踪这种依赖。主流实现通常直接把 consume 按 acquire 处理,它也已经在 C++26 中弃用。
因此它没有值得推荐的日常应用场景。理解设计目的即可,新代码通常直接使用 memory_order_acquire。
五、实际写代码时应该怎么选
实际工程中,可以按照下面的顺序判断:
- 先使用默认的
seq_cst保证正确性。 - 如果只关心原子对象本身,考虑
relaxed。 - 如果原子变量负责发布其他数据,写端使用
release。 - 如果原子变量负责接收已发布的数据,读端使用
acquire。 - 如果读---改---写操作同时需要获取和发布,使用
acq_rel。 - 新代码通常不使用
consume。 - 只有性能测试确认这里是瓶颈,并且能够证明同步关系仍然正确时,才削弱内存序。
可以把最常用的选择压缩成一句话:
只管自己用 relaxed,发布用 release,接收用 acquire,两者都要用 acq_rel,拿不准就用 seq_cst。
六、memory_order 和内存屏障是什么关系
memory_order 是 C++ 语言层面的规则,内存屏障则是编译器和 CPU 实现这些规则时可能使用的手段。
同一个 memory_order_release,在不同处理器上可能生成不同的机器指令。x86 的内存模型相对较强,某些 acquire load 和 release store 不需要额外的硬件屏障;ARM 的内存模型更弱,编译器可能选择带有 acquire/release 语义的指令。
所以,把 release 简单理解成"刷新缓存"、把 acquire 理解成"重新读取内存"并不准确。写 C++ 并发代码时,首先应该判断是否建立了正确的 happens-before 关系,而不是猜测某一款 CPU 会怎样操作缓存。