C++ 原子操作入门
更新时间:2026-08-27。本文是
languages/cpp/主题高手层第 9 篇。上一篇讲了线程和互斥锁,这篇聊更细粒度的同步手段——原子操作。锁是"让线程排队",原子操作是"让单个操作本身不可分割"。理解std::atomic,是理解无锁编程和内存序的起点,也是读多核性能调优文章必备的底子。
本文要回答的问题
- 多线程里
i++为什么不是安全的?"竞态"到底怎么发生的? std::atomic和普通类型用起来有什么差别?fetch_add、compare_exchange这些操作是干什么的?- 内存序
relaxed/acquire/release有什么区别,什么时候用哪个?
先看竞态:i++ 不是一条指令
int counter = 0;
// 两个线程各执行 counter++ 一万次
// 你以为结果是 20000,实际经常不是counter++ 在机器层面至少拆成三步:
- 读:把
counter从内存加载到寄存器 - 改:寄存器里加 1
- 写:把结果存回内存
两个线程交叉执行时,完全可能都读到同一个旧值、都加 1、都写回——于是两次 ++ 只生效了一次。这就是数据竞争(data race),C++ 标准里它的行为是未定义的。

修复方式有两种:加锁(上一篇的 mutex),或者用原子类型。
std::atomic:让操作不可分割
#include <atomic>
std::atomic<int> counter{0};
// 两个线程各执行 counter++ 一万次,结果一定是 20000std::atomic<int> 对 ++、+=、fetch_add 等操作提供原子性——这些操作在硬件层面要么整体执行完,要么整体不执行,中间状态对其他线程不可见。
2.1 常用操作
| 操作 | 语义 | 对应普通写法 |
|---|---|---|
load() | 原子读取 | x = v; |
store(v) | 原子写入 | v = x; |
fetch_add(n) | 原子加,返回旧值 | old = v; v += n; |
fetch_sub(n) | 原子减,返回旧值 | old = v; v -= n; |
exchange(v) | 原子交换,返回旧值 | old = v; v = x; |
compare_exchange_weak/strong | CAS:相等才交换 | if (v == expect) v = x; |
++ / -- / += | 运算符重载 | 同上 |
2.2 CAS:无锁数据结构的基石
**CAS(Compare-And-Swap,比较并交换)**是无锁编程的核心原语:
std::atomic<int> value{0};
int expected = 0;
bool ok = value.compare_exchange_strong(expected, 5);
// 如果 value 仍是 0:换成 5,ok = true
// 如果 value 已变:不换,expected 更新为当前值,ok = falseCAS 的典型用法是"循环重试"——乐观地假设值没变,变了就重来:
std::atomic<int> counter{0};
void increment() {
int old = counter.load();
while (!counter.compare_exchange_weak(old, old + 1)) {
// 失败说明别的线程抢先改了,old 已被更新,重试
}
}2.3 原子类型的限制
| 限制 | 说明 |
|---|---|
| 只支持标量 | int/long/bool/指针 等,不能直接对 struct 用(可用引用计数场景的 shared_ptr) |
| 不支持拷贝赋值 | atomic<int> b = a; 编译错误,只能 b.store(a.load()) |
| 需要硬件支持 | x86 对对齐的 int/long 有原生指令;宽类型(如 double)可能退化为内部加锁 |
不能 memcpy | 拷贝构造函数被删除 |
内存序:原子操作背后的"顺序魔法"
原子操作保证"不可分割",但不保证顺序。std::atomic 的每个操作可指定内存序(memory order),从宽松到严格:
| 内存序 | 含义 | 开销 |
|---|---|---|
memory_order_relaxed | 只保证原子性,不保证顺序 | 最小 |
memory_order_acquire | 读操作:之后的读/写不能重排到它前面 | 中 |
memory_order_release | 写操作:之前的读/写不能重排到它后面 | 中 |
memory_order_acq_rel | acquire + release 组合(RMW 用) | 中高 |
memory_order_seq_cst | 全局一致顺序(默认) | 最高 |
std::atomic<int> flag{0};
std::atomic<bool> ready{false};
int data = 0;
// 线程 A:写数据,然后置 ready
data = 42;
ready.store(true, std::memory_order_release); // 释放写
// 线程 B:等 ready,再读数据
while (!ready.load(std::memory_order_acquire)) {} // 获取读
// 这里读 data 一定是 42release/acquire 配对是经典的生产者-消费者模式:release 保证"发布之前的所有写入"在 acquire 读到标志后可见。这正是互斥锁内部做的事情——锁本身就是 acquire/release 的封装。
// 默认 memory_order_seq_cst 也行(更强),但 release/acquire 更省
ready.store(true); // 等价于 seq_cst
while (!ready.load()) {}原子 vs 互斥锁
| 对比项 | std::atomic | std::mutex |
|---|---|---|
| 粒度 | 单变量 | 代码段 |
| 适用场景 | 计数器、标志位、单个值 | 复杂数据结构、多步骤操作 |
| 性能 | 快(无系统调用) | 慢(可能陷入内核) |
| 正确性 | 容易出错(顺序/可见性) | 简单(临界区全保护) |
| 死锁风险 | 无 | 有 |
工程建议:默认用锁,只有证明锁是瓶颈且场景适合(计数器、标志位、无锁队列)才用原子。 原子操作的正确性靠人肉推理内存序,比锁难得多。
与本站性能主线衔接
- 多核扩展性:原子操作是"避免锁竞争"的手段,锁竞争是 并发性能调优 的核心痛点。
- 缓存一致性:原子操作的可见性依赖缓存一致性协议(MESI),见 L3 缓存一致性。
- 性能剖析:伪共享(false sharing)会让原子操作性能暴跌,可用 perf 的 cache-misses 观察。
- 无锁队列:高性能网络库的 SPSC 队列正是 CAS + 内存序的实践。
一句话总结
原子操作让单个操作不可分割:std::atomic 的 load/store/fetch_add/CAS 解决竞态,内存序(relaxed/acquire/release/seq_cst)控制可见性顺序;能用锁解决就别用原子,原子是性能极致场景的最后手段。