原子操作与内存序 —— std::atomic 六种内存序详解
承 memory-model(C/C++ 内存模型总纲)。本篇展开总纲的"① 内存序"线:为什么程序会被编译器/CPU 重排、可见性/有序性/原子性三特性、
std::atomic的三大类六种内存序、以及生产-消费者模型怎么配对。内存屏障与 x86/ARM 架构差异见 memory-model-fences-arch。
一、内存模型概述:为什么需要它
1. 两个"看不见"的重排来源
写多线程程序时,你以为代码是一行行顺序执行的——但实际执行顺序可能完全不同,原因有二:
- 编译器重排:编译器为了填满流水线、消除等待,会调整内存操作的顺序(只要它认为"单线程内结果不变")。这个判断基于单线程视角,多线程下就暴露了。
- CPU 重排:CPU 乱序执行 + store buffer/invalidate queue 引入的读写乱序,见 reordering-overview。
内存模型(C++11 正式引入)就是规定"在什么情况下,一个线程的内存操作对另一个线程可见、以及按什么顺序可见"的规则。没有它,多线程编程全靠运气。
注意:这里的"内存模型"说的是多线程可见性规范,不是虚拟内存布局。进程地址空间(代码段/数据段/堆/栈)见 segment-management,两个概念别混。
2. 三个关键特性
- 可见性(visibility):一个线程的修改,另一个线程最终能否看到。普通变量的修改没有保证——CPU 缓存可能一直留着旧值。
- 有序性(ordering):内存操作执行的相对顺序是否符合程序预期。编译器/CPU 可能重排。
- 原子性(atomicity):操作要么完整执行、要么完全不执行,不可被切分。
a = a + 1在机器层面是"读-改-写"三步,不是原子的。
普通变量三条全不保证;std::atomic 保证原子性,有序性/可见性按所选内存序(memory order)给。
二、std::atomic 基本用法
std::atomic<T> 让一个变量获得原子操作能力,且默认(seq_cst)附带最强的可见性与有序性保证:
#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> counter = 0;
void increment() {
for (int i = 0; i < 1000000; ++i) {
counter++; // 原子自增:读-改-写一气呵成
}
}
int main() {
std::thread t1(increment), t2(increment);
t1.join(); t2.join();
std::cout << counter << std::endl; // 安全输出 2000000
}底层:原子操作依赖 mesi-protocol 的"缓存锁"(先把 cache line 抢成 Modified 独占再读改写),x86 上对应
lock前缀指令。完整的实现机制见 atomic。
支持的操作一览:
| 操作 | 含义 |
|---|---|
store(v) | 原子写入 |
load() | 原子读取 |
exchange(v) | 原子交换(返回旧值) |
compare_exchange_weak/strong | 原子比较交换(CAS,无锁数据结构核心) |
fetch_add/fetch_sub | 原子加减(返回旧值) |
fetch_and/or/xor | 原子位操作 |
三、六种内存序:三大类
C++11 定义六种内存序,按强度分三大类:
| 类别 | 枚举值 | 强度 | 典型场景 |
|---|---|---|---|
| 顺序一致 | memory_order_seq_cst(默认) | 最强 | 默认选择,全局单序 |
| 获取-释放 | acquire / release / acq_rel | 中 | 生产者-消费者同步 |
| 松散 | relaxed | 最弱 | 纯计数(无同步需求) |
1. seq_cst(顺序一致)——默认
最强的保证:所有操作按程序顺序执行,且全局存在一个所有线程都一致的执行顺序(total order)——像所有线程共用一份"程序序"。
std::atomic<int> data = 0;
std::atomic<bool> ready = false;
void producer() {
data.store(42, std::memory_order_seq_cst); // ①
ready.store(true, std::memory_order_seq_cst); // ②
}
void consumer() {
while (!ready.load(std::memory_order_seq_cst)); // ③
std::cout << data.load(std::memory_order_seq_cst) << std::endl; // 保证输出 42
}语义:② 在 ① 之后,consumer 看到 ②(ready==true)时,①(data=42)也一定已可见。
现实说明:x86 上 seq_cst 的 store 会降级为
mov+mfence(或编译器安排),成本比 relaxed 高。默认用它是"正确性优先",量化的开销差异见 性能量化篇 第五节。
2. acquire / release(获取-释放)
- release 是"发布":
store(release)保证它之前的所有内存操作(含普通写)不会重排到它之后——别人一旦看到这个 store,就一定能看到它之前的所有写。 - acquire 是"获取":
load(acquire)保证它之后的所有内存操作不会重排到它之前——一旦看到那个 store,后续读到的都是最新值。
两者必须配对使用,构成 happens-before 同步关系(这是 C++ 内存模型的核心机制,Go 内存模型里同名的 happens-before 见 Go 内存模型)。
std::atomic<int> data = 0;
std::atomic<bool> ready = false;
void producer() {
data.store(42, std::memory_order_release); // 发布:data 的写在 ready 之前"被看到"
ready.store(true, std::memory_order_release);
}
void consumer() {
while (!ready.load(std::memory_order_acquire)); // 获取:看到 ready 后,
std::cout << data.load(std::memory_order_acquire) << std::endl; // 一定能读到 42
}关键认知:同步的媒介是 ready 这个原子变量,但传递的是"它之前所有写"(包括非原子的 data)。这就是为什么"用 ready 标志 + data 载荷"是经典模式——data 不需要是原子,只要被 release/acquire 的"屏障效应"保护。
3. relaxed(松散)——最弱
只保证操作本身是原子的,不提供任何可见性/有序性保证。适合"结果精确不重要、只要不撕裂"的计数:
std::atomic<int> counter = 0;
void increment() {
for (int i = 0; i < 1000000; ++i) {
counter.fetch_add(1, std::memory_order_relaxed); // 只保证加法原子
}
}典型用途:统计打点、per-thread 计数器累加(每个线程只写自己的计数器,最后合并)、无锁数据结构内部的高频计数。不要用它做标志位同步——下节陷阱就是反例。
四、常见陷阱:relaxed 当标志位
// 错误示例:relaxed 无法保证可见性
std::atomic<int> data = 0;
std::atomic<bool> ready = false;
void producer() {
data.store(42, std::memory_order_relaxed);
ready.store(true, std::memory_order_relaxed); // 可能被重排到 data 之前
}
void consumer() {
while (!ready.load(std::memory_order_relaxed));
std::cout << data.load(std::memory_order_relaxed) << std::endl; // 可能输出 0!
}relaxed 之间没有 happens-before 关系:producer 的两个 store 可以重排,consumer 看到 ready 时 data 未必已写。跨线程传递数据必须用 acquire/release 或 seq_cst。
一句话总结
内存模型规范"一核的写何时对另一核可见":普通变量三特性全无,std::atomic 保证原子性;同步靠内存序——默认 seq_cst(全局单序,最贵)、生产-消费配 release/acquire(happens-before 传递"之前的所有写")、纯计数才用 relaxed;内存屏障与 x86/ARM 差异见 memory-model-fences-arch。