C++
内存序
发布于 2026年7月22日
内存序
C++ 的原子操作不仅保证“读写不可分割”,还通过 std::memory_order 规定:当前线程中的普通内存访问,何时、以什么方式对其他线程可见。
理解内存序的核心不是“CPU 有没有重新排序”,而是:一个线程中的写入,另一个线程在什么条件下被保证能观察到。
1. 为什么需要内存序
下面是一个经典的线程通信例子:
#include <atomic>
#include <cassert>
int data = 0;
std::atomic<bool> ready{false};
void producer()
{
data = 42;
ready.store(true, std::memory_order_release);
}
void consumer()
{
while (!ready.load(std::memory_order_acquire)) {
}
assert(data == 42);
}
这里有两个问题:
ready是原子的,因此对它的读写不会产生数据竞争。data不是原子的,但 release/acquire 建立了跨线程同步,因此读取data是安全的。
同步链条为:
data = 42
↓ sequenced-before
ready.store(true, release)
↓ synchronizes-with
ready.load(acquire)
↓ sequenced-before
读取 data
当 acquire load 读到了 release store 写入的值时,release 之前的操作 happens-before acquire 之后的操作。
2. 六种内存序总览
| 内存序 | 原子性 | 排序与同步能力 | 常见用途 |
|---|---|---|---|
relaxed |
有 | 不提供跨线程同步 | 计数器、统计信息 |
consume |
有 | 数据依赖排序 | 实践中通常不建议使用 |
acquire |
有 | 后续操作不能越过它 | 接收其他线程发布的数据 |
release |
有 | 此前操作不能越过它 | 发布数据 |
acq_rel |
有 | 同时 acquire 与 release | 原子读改写操作 |
seq_cst |
有 | 额外提供全局一致顺序 | 默认、安全优先 |
3. memory_order_relaxed
relaxed 只保证:
- 当前原子对象的操作是原子的;
- 当前原子对象存在一致的修改顺序;
- 不保证其他普通变量的可见性;
- 不建立线程间同步关系。
std::atomic<int> counter{0};
void work()
{
counter.fetch_add(1, std::memory_order_relaxed);
}
这适合统计完成次数。每次递增不会丢失,但不能通过 counter 推断其他数据已经准备完成。
错误示例
int data = 0;
std::atomic<bool> ready{false};
void producer()
{
data = 42;
ready.store(true, std::memory_order_relaxed);
}
void consumer()
{
if (ready.load(std::memory_order_relaxed)) {
std::cout << data; // 数据竞争,行为未定义
}
}
即使消费者读到 ready == true,也没有同步关系保证它能安全读取 data。
relaxed 并非“完全无序”
对同一个原子变量,所有修改仍存在统一的 modification order:
std::atomic<int> x{0};
x.store(1, std::memory_order_relaxed);
x.store(2, std::memory_order_relaxed);
其他线程不能把这两个修改解释成先 2 后 1。但不同原子变量之间没有统一顺序保证。
4. memory_order_release
release 通常用于原子写操作:
flag.store(true, std::memory_order_release);
它保证当前线程中位于 release 之前的读写,不能在 C++ 抽象机意义上移动到 release 之后。
release 主要用于“发布”数据:
payload = create_payload();
ready.store(true, std::memory_order_release);
release 不意味着“立即刷新所有缓存”。它规定的是:一旦匹配的 acquire 观察到了这个发布操作,之前的数据也必须可见。
5. memory_order_acquire
acquire 通常用于原子读操作:
if (flag.load(std::memory_order_acquire)) {
use_shared_data();
}
它保证 acquire 之后的操作不能移动到 acquire 之前。
只有当 acquire load 确实读到对应 release store 或相应 release sequence 的结果时,二者才建立同步。如果 load 读到旧值,则没有同步关系。
6. memory_order_acq_rel
acq_rel 同时具有 acquire 和 release 语义,主要用于读—改—写操作:
counter.fetch_add(1, std::memory_order_acq_rel);
flag.exchange(true, std::memory_order_acq_rel);
state.compare_exchange_strong(
expected,
desired,
std::memory_order_acq_rel
);
- release 部分:发布当前操作之前的内容。
- acquire 部分:接收当前操作所读取到的其他线程内容。
典型场景包括自旋锁、引用计数、无锁链表与队列、并发状态机。
普通 load 不能使用 release 或 acq_rel;普通 store 不能使用 acquire 或 acq_rel。
7. memory_order_seq_cst
seq_cst 是 sequentially consistent(顺序一致性),也是默认内存序:
x.store(1); // 默认 seq_cst
int value = x.load();
它包含 acquire/release 能力,并为所有 seq_cst 操作建立一条所有线程都同意的全局总顺序。
std::atomic<bool> x{false};
std::atomic<bool> y{false};
int r1;
int r2;
void thread1()
{
x.store(true, std::memory_order_seq_cst);
r1 = y.load(std::memory_order_seq_cst);
}
void thread2()
{
y.store(true, std::memory_order_seq_cst);
r2 = x.load(std::memory_order_seq_cst);
}
这里不允许同时出现:
r1 == false && r2 == false
如果全部改成 relaxed,则两个线程都读到 false 是允许的。
seq_cst 并不是“禁止所有优化”,而是要求程序的可观察行为能解释为一个符合规则的全局顺序。
8. memory_order_consume
consume 原本用于提供比 acquire 更弱的数据依赖排序:
Node* p = head.load(std::memory_order_consume);
int value = p->value;
但数据依赖传播规则非常复杂,编译器长期难以正确、高效地实现。实践中编译器通常把 consume 当作 acquire。
工程中建议直接使用:
head.load(std::memory_order_acquire);
9. 三个重要关系
9.1 sequenced-before
描述同一线程内的顺序:
data = 42;
flag.store(true, std::memory_order_release);
data = 42 sequenced-before flag.store(...)。
9.2 synchronizes-with
描述特定原子操作形成的跨线程同步:
release store synchronizes-with 读取该结果的 acquire load。
9.3 happens-before
这是判断普通内存访问是否安全的关键关系:
线程内 sequenced-before
+
跨线程 synchronizes-with
+
线程内 sequenced-before
=
happens-before
两个线程对同一个普通变量进行冲突访问,其中至少一个是写,并且不存在 happens-before,就构成 data race。C++ 规定数据竞争是未定义行为。
10. compare_exchange 的成功与失败内存序
bool compare_exchange_weak(
T& expected,
T desired,
std::memory_order success,
std::memory_order failure
);
常见用法:
state.compare_exchange_weak(
expected,
desired,
std::memory_order_acq_rel,
std::memory_order_acquire
);
- 成功时发生写入,使用成功内存序。
- 失败时不发生写入,当前值被写回
expected,使用失败内存序。 - 失败内存序不能是
release或acq_rel,因为失败时没有内容可发布。
compare_exchange_weak 允许伪失败,因此通常放在循环中:
int expected = value.load(std::memory_order_relaxed);
while (!value.compare_exchange_weak(
expected,
expected + 1,
std::memory_order_relaxed,
std::memory_order_relaxed)) {
// 失败后 expected 已更新为 value 的当前值
}
11. Release sequence
std::atomic<int> flag{0};
int data;
void thread1()
{
data = 42;
flag.store(1, std::memory_order_release);
}
void thread2()
{
flag.fetch_add(1, std::memory_order_relaxed);
}
void thread3()
{
if (flag.load(std::memory_order_acquire) == 2) {
assert(data == 42);
}
}
thread2 的 fetch_add 是原子读改写操作。即使它使用 relaxed,也可以成为 release sequence 的一部分。
当 thread3 的 acquire load 读到这条 release sequence 中的值时,仍可与最初的 release store 建立同步,从而看到 data == 42。
12. 原子变量不会自动保护普通变量
int x = 0;
std::atomic<int> counter{0};
void writer()
{
x = 1;
counter.fetch_add(1, std::memory_order_relaxed);
}
void reader()
{
if (counter.load(std::memory_order_relaxed) > 0) {
std::cout << x; // 不安全
}
}
counter 是原子的,只保证 counter 自身没有数据竞争,并不会自动保护 x。
正确的发布与获取:
void writer()
{
x = 1;
counter.store(1, std::memory_order_release);
}
void reader()
{
if (counter.load(std::memory_order_acquire) == 1) {
std::cout << x; // 安全
}
}
13. 自旋锁示例
class SpinLock {
public:
void lock()
{
while (locked_.exchange(true, std::memory_order_acquire)) {
}
}
void unlock()
{
locked_.store(false, std::memory_order_release);
}
private:
std::atomic<bool> locked_{false};
};
加锁成功时使用 acquire,用于获取上一个持锁线程发布的数据;解锁使用 release,保证临界区中的修改在解锁前完成。
14. atomic_thread_fence
std::atomic_thread_fence(std::memory_order_release);
std::atomic_thread_fence(std::memory_order_acquire);
栅栏可以对前后的多个操作施加排序,但正确建立跨线程同步仍需要原子操作参与。规则通常比直接使用 release store 和 acquire load 更难推理,因此除非实现底层并发组件,否则优先把内存序直接附着在原子操作上。
15. 编译器屏障与 CPU 屏障
内存序同时约束两个层面:
- 编译器不能生成违反 C++ 内存模型的重排;
- 机器指令必须在目标 CPU 上满足相应语义。
不同架构的成本不同:
- x86/x64 内存模型较强,很多 acquire load、release store 不需要额外屏障指令;
- ARM、POWER 等架构较弱,可能需要特殊加载、存储或屏障;
- seq_cst 通常可能比 acquire/release 成本更高。
不要只根据某次 x86 汇编结果判断源代码是否正确。代码首先必须符合 C++ 内存模型。
16. 如何选择内存序
默认选择 seq_cst
如果性能没有被测量证明存在问题,默认的 seq_cst 最容易推理。
C++ 的 std::atomic 如果不指定内存序,默认使用:
std::memory_order_seq_cst
发布数据:release/acquire
data = value;
ready.store(true, std::memory_order_release);
// 另一线程
if (ready.load(std::memory_order_acquire)) {
use(data);
}
仅关心原子值本身:relaxed
request_count.fetch_add(1, std::memory_order_relaxed);
适用于统计计数、调试指标等不承担数据发布职责的场景。
读改写既接收又发布:acq_rel
state.fetch_or(mask, std::memory_order_acq_rel);
是否需要双向同步,必须根据算法证明,不能只凭“它看起来更强”。
17. 常见误区
volatile 可以实现线程同步
不可以。C++ 的 volatile 不提供原子性,也不建立 happens-before。
原子操作一定无锁
不一定:
std::atomic<T> value;
bool lock_free = value.is_lock_free();
原子性与无锁性是两个概念。
relaxed 就是不安全
不是。它对原子对象本身是安全的,只是不适合承担普通数据的发布和接收职责。
acquire 能看到所有线程的最新数据
不能这样理解。它只保证看到与匹配 release 之间由 happens-before 覆盖的效果。
更强的内存序一定能修复并发算法
不一定。内存序无法修复生命周期错误、ABA 问题、越界或错误的状态转换。
18. 记忆口诀
relaxed:只保证这个原子变量本身。
release:把此前的数据发布出去。
acquire:接收另一个线程此前发布的数据。
seq_cst:acquire/release 加上一条全局统一的原子操作顺序。
最后记住关键条件:
acquire 必须观察到相应的 release 写入或其 release sequence,二者才建立同步关系。
在工程中,除非正在实现高性能无锁结构,否则优先使用互斥锁或默认的 seq_cst。弱化内存序之前,应先写出明确的 happens-before 证明,再通过基准测试确认弱化确实带来了收益。