1.三个原子类型
1.1.std::atomic_flag
最简单、保证无锁(is_lock_free() 恒为 true)的原子布尔标志。
c
std::atomic_flag f = ATOMIC_FLAG_INIT; // 唯一合法初始化方式(C++20 前)
// C++20 起可用 std::atomic_flag f; 默认初始化为 clear
- 只有两个操作:
test()(C++20,读)、clear()(写,参数为memory_order,不允许release/acq_rel/seq_cst以外?------准确说clear只允许relaxed/release/seq_cst)、test_and_set()(RMW,读-改-写)。 C++20前没有test(),所以它是一个纯"测试并设置"的标志位,用于实现自旋锁:
c
class spinlock {
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock() { while (flag.test_and_set(std::memory_order_acquire)) {} }
void unlock() { flag.clear(std::memory_order_release); }
};
acquire限制了lock后,临界区的内容不会重排序到lock前,release限制了unlock前临界区的内容不会重排到unlock后。
- 不能拷贝、赋值、交换。
1.2.std::atomic(T 为整型/指针/浮点等 trivially copyable 类型)
通用原子封装,操作:load / store / exchange / compare_exchange_strong / weak / fetch_add / fetch_sub / fetch_and / fetch_or / fetch_xor / operator++ 等。
- 不可拷贝、不可赋值(拷贝构造/拷贝赋值被删除),但有一个非原子的
operator=(T)(等价于store(r, seq_cst))。 - 可能不是无锁的(用锁实现),可用
is_lock_free()查询。
1.3.std::atomic<T*>(特化)
指针特化,支持所有通用操作,外加指针算术:
c
std::atomic<int*> p;
p.fetch_add(3); // 前进 3 * sizeof(int)
p.fetch_sub(1); // 后退
p += 2; p -= 1; // 等价形式
++p; --p; // 自增自减
fetch_add/fetch_sub是RMW操作,返回修改前的旧指针。- 注意内存序约束:指针运算只保证对指针值本身的原子性,不保证指针所指对象的生命周期安全(
ABA问题的根源之一)。
2.五种基本操作与 memory_order 矩阵
六种 memory_order:
| 顺序 | 归类 |
|---|---|
relaxed |
无同步 |
consume |
load 类(C++26 已弃用/移除提案,实践中等价 acquire) |
acquire |
load 类 |
release |
store 类 |
acq_rel |
RMW 专用 |
seq_cst |
全能,默认且最强 |
约束规则(编译期强制):
store只允许:relaxed / release / seq_cstload只允许:relaxed / consume / acquire / seq_cstRMW允许全部六种- 传错顺序是
UB(运行期断言/编译期检查实现定义,但标准上违规即未定义行为)
2.1.load 四种顺序
c
T load(memory_order order = seq_cst) const noexcept;
| 顺序 | 语义 |
|---|---|
relaxed |
仅保证"读到某个时刻的真实值"(原子性),无任何顺序保证。可读到任意时刻的值,不与其他操作排序。 |
consume |
保证对该原子变量依赖该读值 的后续操作不被重排到读之前(data dependency ordering)。允许硬件上比 acquire 更便宜的优化(如 ARM/PowerPC 上不需要内存屏障)。注意:编译器常把 consume 提升为 acquire(std::kill_dependency 也几乎无用),C++26 起 consume 被移除/弃用,实践中直接用 acquire。 |
acquire |
建立"获取"语义:本线程中所有 后续(程序顺序在后)的读写不能被重排到本次 load 之前。典型用途:读取标志位后安全读取共享数据。 |
seq_cst |
acquire 全部语义 + 全局单一总顺序(single total order)。所有线程看到的 seq_cst 操作序列一致。最慢(x86 上 load 的 seq_cst 通常免费,store 需要 xchg/mfence)。 |
2.2.store 三种顺序
c
void store(T desired, memory_order order = seq_cst) noexcept;
| 顺序 | 语义 |
|---|---|
relaxed |
原子地写入,其他线程不会读到撕裂值,但无发布语义。前后代码可自由穿越这个 store 重排。 |
release |
建立"释放"语义:本线程中所有 之前的读写不能被重排到本次 store 之后。与另一个线程的 acquire-load 形成 happens-before(配对使用)。 |
seq_cst |
release 语义 + 参与全局总顺序。保证"所有 seq_cst store 按同一顺序被所有线程观察到"。 |
释放-获取配对(Dekker/Peterson 式同步的基石):
c
// 线程 1 // 线程 2
data = 42; if (flag.load(acquire)) {
flag.store(true, release); ──▶ // data 必为 42(happens-before)
}
2.3.exchange(RMW)
c
T exchange(T desired, memory_order order = seq_cst) noexcept;
原子地"写入新值并返回旧值",一步到位。任何顺序都允许,因为它同时是读和写:
- 只用
relaxed:当只需原子交换、不需要发布数据时; - 用
acq_rel:若既要从旧值"获取"、又要向新值"释放"; - 用
seq_cst:参与全局总顺序(默认)。
2.4.compare_exchange_strong / weak(CAS)
c
bool compare_exchange_strong(T& expected, T desired, memory_order success, memory_order failure) noexcept;
bool compare_exchange_strong(T& expected, T desired, memory_order order = seq_cst) noexcept; // failure = order 但不得强于 success
bool compare_exchange_weak (同上);
语义:原子地执行 if (current == expected) { current = desired; return true; } else { expected = current; return false; }
- 失败时
expected会被改写为当前值------这是用CAS循环更新状态的机制。 - 失败序必须是 load 类顺序(
relaxed/consume/acquire/seq_cst),且不允许强于成功序。
strong vs weak 的区别:
| strong | weak | |
|---|---|---|
| 假失败(spurious failure) | 不允许 | 允许 (在某些平台如 ARM/LL-SC 架构上,即使 *this == expected 也可能返回 false) |
| 开销 | 可能更重(需要内部循环保证) | 更贴近硬件,单条 LL/SC 指令 |
| 使用场景 | 只调用一次、不能容忍假失败 | 写循环时 (必须包在 while 里) |
c
// weak 必须这样用:
int old = head.load(relaxed);
while (!head.compare_exchange_weak(old, new_node,
memory_order_release,
memory_order_relaxed)) {
// old 已被刷新为最新值,循环继续
}
无锁栈的 push 就依赖这个模式。
3.RMW 六种顺序逐一解析
RMW(read-modify-write,如 fetch_add、exchange、CAS、++x)的语义 = "读的顺序" + "写的顺序"的组合:
| 顺序 | 读侧语义 | 写侧语义 | 典型场景 |
|---|---|---|---|
relaxed |
无 | 无 | 纯计数器(统计调用次数),只需结果不丢更新 |
consume |
数据依赖序 | ---(C++ 标准对 RMW 的 consume 写侧无意义) | 罕见,一般直接用 acquire |
acquire |
获取语义 | 无 | 读旧值以判断状态,但不发布新数据 |
release |
无 | 释放语义 | 发布新数据,但不关心读到什么旧值 |
acq_rel |
获取 + 释放 | 同左 | 同时拥有:读到旧值后开始临界区(acquire),写入新值结束临界区(release)。无锁引用计数增减的经典选择 |
seq_cst |
获取 + 全局总序 | 释放 + 全局总序 | 需要跨线程严格一致顺序时(如 Dekker 算法互斥双方的两个 RMW) |
c
// 引用计数 release 场景
void unref() {
if (count.fetch_sub(1, std::memory_order_acq_rel) == 1)
delete this; // 我是最后一个:之前的 release 操作(其他线程的 store)
} // 已同步,可以安全析构
关键点:fetch_add(1, memory_order_release) 的含义是"本次加法结果对其他做过 acquire 的线程可见",而不是"加法操作之前的代码对任何线程有序"------它是单边的。
4.std::atomic 对用户自定义类型的要求(C++20 前 vs C++20 起)
C++11/14/17:POD 限制
std::atomic<T> 对自定义 T 的(C++17 及之前)要求是:
is_trivially_copyable_v<T>为true(即可以用memcpy复制、无自定义拷贝构造/虚函数等);- 可复制构造、可移动赋值(std::memcpy 语义);
- 所有基类子对象和数据成员都满足上述条件;
- 不能是数组(指针可以);
compare_exchange用bitwise比较(memcmp语义),而不是operator==------因此不能有padding中的不确定值,否则CAS可能永远失败(padding字节每次拷贝不同)。T必须平凡默认构造(C++17起要求is_default_constructible的平凡默认构造)。- 需要
atomic<T>特化可用,否则操作=全局std::atomic<T>模板的默认实现(用内部锁),is_lock_free()返回 false。
C++20 起:要求放宽
std::atomic<T> 的合法 T 变为:任何 is_trivially_copyable_v<T> 且可平凡默认构造的类型------即 union、含位域的结构体(bit-field)等也变得可用,并且 volatile 限定语义整理。
C++20 新增 std::atomic_ref<T>:对已存在对象的原子引用(不要求对象本身是 atomic),要求同样是 trivially copyable。
实践中自定义原子类型的正确姿势
c
struct Config {
int a;
double b;
char name[16];
// 不能有 virtual、不能有自定义拷贝/析构
// 注意对齐问题:
} __attribute__((aligned(8))); // 让 sizeof 对齐到 8 的倍数,减少 padding 差异
static_assert(std::is_trivially_copyable<Config>::value, "");
std::atomic<Config> cfg;
padding 陷阱:
c
struct Bad {
char c; // 1 字节 + 7 字节 padding
int i;
};
std::atomic<Bad> x;
Bad expected = x.load();
while (!x.compare_exchange_weak(expected, new_val)) {} // 可能死循环!padding 每次不同
解决方案:手工填充(char pad[7])、alignas、或改用成员级原子变量。
C++26 视角(当前时间点):
std::atomic_flag 的 test() 已加入;memory_order_consume 和 std::kill_dependency 已被弃用(实践中编译器把 consume 一律升级为 acquire,标准委员会放弃了修复它的努力);
atomic<T> 的自定义类型要求保持不变(trivially copyable)。
5.一张总表:每种操作允许的顺序
| 操作 | relaxed | consume | acquire | release | acq_rel | seq_cst |
|---|---|---|---|---|---|---|
store |
✓ | ✓ | ✓ | |||
load |
✓ | ✓ | ✓ | ✓ | ||
exchange |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
compare_exchange_* |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
fetch_add/sub/and/or/xor |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
默认全是 seq_cst------不写顺序就是最强的,写无锁代码时这是最常见的性能 bug 来源(x86 上 store 的 seq_cst 比 release 多一条 mfence/lock xchg,ARM 上差距更大)。
6.选择建议(实战)
- 计数器(只统计,不发布数据):
fetch_add(1, relaxed)。 - 一次性初始化标志位:
store(true, release) + load(acquire)(比seq_cst的call_once更便宜)。 - 无锁队列/栈的
push(发布节点):CAS用release或acq_rel,失败侧relaxed。 - 自旋锁:
test_and_set(acquire) / clear(release)。 - 只有当你需要"所有线程看到完全一致的交错顺序"(如实现
Dekker互斥)才用seq_cst。 - 永远不要手动写
consume,直接acquire。
7.内存顺序语义详细解释
7.1.relaxed ------ 唯一的规则是"不撕裂"
c
前面的代码 ┆ 这个原子操作 ┆ 后面的代码
(随便穿越,随便换序)
- 只保证一件事:这个读/写本身是原子的,别的线程不会读到"写了一半的值"(对
int不会出现0x00001234和0x56780000拼出来的怪值)。 - 前后代码可以任意穿越它重排。比如
x.store(1, relaxed)前后算好的普通变量,别的线程读到 1 时完全可能还没算好。
7.2.acquire ------ "后面的不许跑到我前面"(只约束后半边)
c
前面的代码 → 这个原子读 ║←屏障→║ 后面的代码
(可以穿越下去) (不许穿越上来)
- 规则:本操作中之后的所有读写,禁止重排到本操作之前。
- 含义:我"获取"到了某个信号(读到了某个值),那么读信号之后做的事,逻辑上必须真的发生在读信号之后。
7.3.release ------ "前面的不许跑到我后面"(只约束前半边)
c
前面的代码 ║←屏障→║ 这个原子写 → 后面的代码
(不许穿越下去) (可以穿越上来)
- 规则:本操作之前的所有读写,禁止重排到本操作之后。
- 含义:我把数据都算好、摆好了(前面的代码),然后"释放"出一个信号(写入标志位)。别人看到信号时,数据必须已经摆好了。
7.4.acq_rel ------ 同时是 acquire 又是 release(两头都堵)
c
前面的代码 ║←屏障→║ 读+写 ║←屏障→║ 后面的代码
(不许下去) (不许上来)
- 前面不许穿下去 + 后面不许穿上来。
- 关键:它只约束本线程内的重排,不约束"所有线程看到的全局顺序"------这是它和
seq_cst的本质区别。
7.5.seq_cst ------ acq_rel 再加强制"全球统一剧本"
- 拥有
acq_rel的全部约束; - 额外加一条:所有线程的所有
seq_cst操作,在所有线程眼中都按同一个顺序发生。
c
// 线程1 // 线程2
a.store(1, seq_cst); b.store(1, seq_cst);
r1 = b.load(seq_cst); r2 = a.load(seq_cst);
7.6.结合硬件底层解释seq_cst与acq_rel
每个核心里有一个 store buffer(写缓冲)------一个几十项的小队列。执行 x = 1 时:
- 核把
x=1放进自己的store buffer,指令立刻"退休",流水线继续跑下一条; - 缓冲里的写稍后才异步发给缓存系统,真正变成"全局可见"可能滞后几十到几百个周期;
也就是说:"指令执行了" ≠ "别的核能看见"。写有一个"到达全球"的时差。
seq_cst 的 store 在硬件上等于"带全屏障的写":
x86:lock前缀指令(如lock xchg)或mfence。作用:排空本核整个写缓冲,等所有先前的写真正进入缓存系统、拿到coherence point的确认,store才退休。ARM:stlr之外还需dmb ish级全屏障(RMW形式的seq_cst常在指令后再补一道)。
效果体现在写的一端:执行 x.store(1, seq_cst) 的核必须站在原地等 x=1 全球落地,才能执行下一条指令。cst下store操作既修改所在核的store buffer,还需同步执行发送修改给缓存系统,达成缓存一致性,才继续下一条指令。
这里还有几点需要注意:
- 核
T上的写操作执行了异步发送,但此异步发送被其他各个核所感知到的时间点也会因为核间距离不同而有所差异。 - 但相同的源和目标下,先发出的异步通知,相比后发出的异步通知必然会被目标核先感知到。
- 对写操作,逻辑上通过
release来担保过去。作为对比,对读,逻辑上通过acquire来担保未来。分别担保一半。
8.操作原子语义
| 操作 | 读? | 写? | 为什么 |
|---|---|---|---|
store(x) |
W | 只写入,不管旧值是什么 | |
load() |
R | 只读取,不改变值 | |
exchange(x) |
R | W | 先读旧值,再写入新值,一步完成 |
compare_exchange(e, d) |
R | W | 读当前值来比较,相等则写入 |
fetch_add(1) |
R | W | 读出旧值、写入旧值+1 |
为什么 store 行的空白格是 acquire / consume / acq_rel?
store没有读部分。acquire的语义是"我读到信号了,后面的事不许提前"------store什么都没读,"获取"谁呢?这个约束没有任何意义,标准直接禁止(用了就是 UB)。release有意义:"写之前的准备工作不许重排到写之后"。✓acq_rel = acquire + release,acquire半边无意义 → 禁止。seq_cst有意义:它含release语义 + 全球总序。✓
为什么 load 行的空白格是 release / acq_rel?
load没有写部分。release的语义是"我写出一个信号,之前的准备必须已就位"------load什么都没发布,release无对象。acquire有意义:"我读到信号了,之后的消费不许提前"。✓seq_cst有意义:含acquire+ 全球总序。✓
为什么 RMW 行(exchange / CAS / fetch_*)六个全 ✓?
| 组合 | 读侧约束 | 写侧约束 | 场景 |
|---|---|---|---|
fetch_add(1, relaxed) |
无 | 无 | 纯计数,只求不丢更新 |
fetch_add(1, acquire) |
后面的不许提前 | 无 | 读到旧值后才开始干活,但不对外发布什么 |
fetch_add(1, release) |
无 | 前面准备不许滞后 | 算好东西,写入新值即发布 |
fetch_add(1, acq_rel) |
后面的不许提前 | 前面准备不许滞后 | 一次操作既"进场"又"出场",如引用计数减到 1 后析构 |
fetch_add(1, seq_cst) |
以上全部 + 全球总序 | 同上 | 需要所有线程一致剧本 |
8.std::atomic_thread_fence
8.1.它是什么
c
void atomic_thread_fence(std::memory_order order) noexcept; // <atomic>
一句话:它只施加"顺序约束",本身不是任何读/写/RMW。
- 编译器视角:一道编译屏障------屏障两侧的内存操作不许互相跨越;
- 硬件视角:一道
CPU屏障------视顺序强度插入mfence / dmb等指令; - 它不操作任何变量,必须和(通常
relaxed的)原子变量配合使用才有跨线程意义。
它和"给原子操作传 memory_order"的本质区别:
| 操作自带的顺序 | 独立栅栏 | |
|---|---|---|
| 作用对象 | 只约束那一个原子操作 | 约束它两侧的所有内存操作 |
| 位置 | 焊死在操作上 | 自由摆放------可以在循环外、函数边界、热路径之外 |
| 典型用途 | 常规同步 | 把昂贵的序从热路径挪走,做精细优化 |
8.2.六种顺序的栅栏语义
| 栅栏 | 语义 |
|---|---|
fence(relaxed) |
空操作(只 compiler barrier 级别都算不上,无跨线程效果) |
fence(consume) |
数据依赖序(已弃用,同 acquire) |
fence(acquire) |
之后的所有读写,不许重排到栅栏之前 |
fence(release) |
之前的所有读写,不许重排到栅栏之后 |
fence(acq_rel) |
两者兼有 |
fence(seq_cst) |
acq_rel + 参与全局总序 S |
栅栏是单边约束,配对规则才是灵魂(见下)。
8.3.核心配对规则:栅栏怎么"借力"relaxed 原子操作
栅栏自己不是原子操作,无法被"读到",所以它需要找一个 relaxed 原子操作当载体:
c
线程 A 线程 B
普通写 x = 42; while (y.load(relaxed) != 1) {}
fence(release); fence(acquire);
y.store(1, relaxed); 普通读 r = x; // 保证 42
和标准教科书例子的对照:
c
// 等价于 ready.store(1, release) + ready.load(acquire)
x = 42; // 数据准备
std::atomic_thread_fence(std::memory_order_release);
ready.store(true, std::memory_order_relaxed); // 信号(载体)
// 对端
while (!ready.load(std::memory_order_relaxed)) {}
std::atomic_thread_fence(std::memory_order_acquire);
assert(x == 42); // 保证成立
效果与 release/acquire 版完全等价,区别只在约束的落点:release 版本把顺序焊在 ready 这一次操作上;栅栏版本把顺序摊给了栅栏两侧的一切操作。
8.4.一句话总结
- 原子操作的
memory_order把顺序"焊在操作上"; atomic_thread_fence把顺序"撒在两操作之间"。- 前者够用、好读;后者让你在多变量、热路径、批量同步的场景里,以更低的成本精确铺设
happens-before的边界。