原子操作、内存序与无锁基础
原子操作、内存序与无锁基础
时间:2026/04/09
关键词:
std::atomic、CAS、memory order、acquire/release、seq_cst、false sharing、lock-free
核心目标:搞清楚“原子变量为什么不仅是线程安全的普通变量”,以及不同内存序到底在约束什么。
1. 为什么需要原子操作
并发程序里最核心的问题是:
- 多个线程会同时访问共享数据
- 如果至少一个线程写,且没有同步,就会产生数据竞争
例如:
1 | int counter = 0; |
这不是“结果偶尔不准”,而是未定义行为。
原子操作的价值就在于:
- 某些共享读写可以不加互斥锁
- 但仍然具备明确同步语义
2. std::atomic 是什么
最基本的用法:
1 |
|
它提供的不是“更快的 int”,而是:
- 不会被撕裂的原子读写
- 受内存模型约束的同步语义
常见操作:
loadstorefetch_addfetch_subexchangecompare_exchange_weakcompare_exchange_strong
3. 原子不等于万能替代锁
原子适合的场景通常是:
- 计数器
- 标志位
- 状态发布
- 无锁数据结构中的基本原语
不适合的场景通常是:
- 需要保护一整段复合逻辑
- 需要同时维护多个共享变量的不变式
- 业务逻辑复杂,容易写错同步关系
一句话:
- 原子擅长保护“一个共享状态”
- 锁擅长保护“一个临界区”
4. 最基本的原子操作
4.1 load / store
1 | std::atomic<int> x{0}; |
4.2 fetch_add
1 | std::atomic<int> cnt{0}; |
这比 cnt = cnt + 1 更重要,因为它是一个不可分割的原子读改写。
4.3 exchange
1 | bool old = flag.exchange(true); |
含义:
- 把
flag设为true - 返回旧值
5. CAS:无锁算法的核心原语
CAS 指 Compare-And-Swap(或 Compare-And-Exchange)。
5.1 直觉理解
1 | 如果当前值仍然等于 expected, |
5.2 C++ 里的写法
1 | std::atomic<int> x{0}; |
如果成功:
x变成 1- 返回
true
如果失败:
x保持原值expected会被写成当前实际值
5.3 weak 和 strong
compare_exchange_weak:允许伪失败,适合循环重试compare_exchange_strong:不允许伪失败,语义更直接
常见模式:
1 | int expected = old; |
6. “原子”到底保证了什么
原子操作通常有两层含义:
6.1 操作本身不可分割
例如 fetch_add 不会被拆成“先读、再改、再写”被别的线程插进来。
6.2 它还可能携带同步顺序语义
这就进入内存序问题。
如果只知道“原子不会撕裂”,还远远不够。
7. 为什么会有内存序
现代 CPU 和编译器都会重排指令,只要不改变单线程可观察结果就行。
但并发下,如果没有同步约束,另一个线程看到的顺序可能和源码顺序不一样。
所以原子操作不只是“安全读写”,还承担:
- 约束编译器重排
- 约束 CPU 可见性顺序
这就是 memory_order 的意义。
8. 六种常见内存序
C++ 里最常见的有:
memory_order_relaxedmemory_order_consumememory_order_acquirememory_order_releasememory_order_acq_relmemory_order_seq_cst
实际工程里最常用、最值得掌握的是:
relaxedacquirereleaseacq_relseq_cst
consume 在现代工程里很少主动使用。
9. relaxed:只保证原子性,不保证顺序
1 | counter.fetch_add(1, std::memory_order_relaxed); |
适合:
- 纯计数
- 统计量
- 不需要借此发布其他数据
不适合:
- “先写数据,再置位通知别人读取”
因为 relaxed 不保证其他普通内存写入的可见顺序。
10. release / acquire:发布与获取
这是最重要的一组。
10.1 发布方:release
1 | data = 42; |
含义可以粗略理解为:
- 在这之前的写入,不能被重排到这个
store之后
10.2 获取方:acquire
1 | if (ready.load(std::memory_order_acquire)) { |
如果这个 acquire load 读到了对应的 release store 写入的值,那么发布前的写入也对当前线程可见。
10.3 最经典的发布-订阅模式
1 |
|
这就是 acquire/release 最核心的使用场景。
11. acq_rel:读改写操作常用
对于像 fetch_add、exchange、CAS 这样的读改写操作,经常用:
1 | std::memory_order_acq_rel |
它表示:
- 对之前的写有 release 效果
- 对之后的读有 acquire 效果
适合:
- 需要同时承担“发布 + 获取”角色的 RMW 操作
12. seq_cst:最强也最直观
1 | x.store(1, std::memory_order_seq_cst); |
它提供最强、最容易理解的全局顺序语义。
可以先粗略理解为:
- 所有
seq_cst原子操作在所有线程看来像排成了一条总顺序
优点:
- 最不容易想错
缺点:
- 可能比更弱顺序更保守
经验上:
- 不确定时可以先用
seq_cst - 真有性能证据,再考虑是否降到 acquire/release 或 relaxed
13. 一个常见误区:原子变量保护不了“旁边的普通变量”,除非顺序写对
例如:
1 | int data = 0; |
如果你这样写:
1 | data = 42; |
另一个线程即便读到 ready == true,也不一定能可靠看见 data == 42。
因为这里只保证 ready 本身原子,不保证普通变量 data 的可见顺序。
所以:
- “用一个原子标志通知别人去读普通数据”
这个模式必须认真选内存序
14. 自旋等待与忙等
原子标志很容易写出自旋:
1 | while (!ready.load(std::memory_order_acquire)) { |
这在短等待场景可能可接受,但有风险:
- 浪费 CPU
- 争抢资源
- 等待时间一长非常低效
所以如果等待时间不可控,通常更适合:
- 条件变量
- futex / event
- 更高层并发原语
15. atomic_flag 与自旋锁
最简单的原子标志是:
1 | std::atomic_flag lock = ATOMIC_FLAG_INIT; |
可实现一个最简单自旋锁:
1 | while (lock.test_and_set(std::memory_order_acquire)) { |
但要明确:
- 这只是帮助理解 acquire/release 的经典例子
- 真实工程里自旋锁并不一定是好选择
原因包括:
- 高竞争下性能糟糕
- 容易烧 CPU
- 可能不公平
16. lock-free 不等于更快
这是并发里最常见的误判之一。
“无锁”通常只表示:
- 线程推进不依赖传统互斥锁
它不自动意味着:
- 更低延迟
- 更高吞吐
- 更少 cache traffic
实际上,原子和 CAS 往往会带来:
- cache line 抖动
- 重试开销
- 更难维护的代码
所以工程经验是:
先用正确、清晰的同步方案,再证明锁真的成了瓶颈,才考虑无锁化。
17. false sharing 在原子场景里更常见
多个线程频繁更新不同的原子变量,如果这些变量落在同一个 cache line,也会很慢。
例如:
1 | struct Counters { |
如果两个线程分别只改 a 和 b,仍可能严重互相干扰。
常见缓解方式:
1 | struct alignas(64) Counter { |
或者:
- 每线程本地累加
- 最后统一合并
18. 原子引用计数与 shared_ptr
shared_ptr 之所以比 unique_ptr 重,很大一部分原因就在于:
- 它内部常涉及线程安全的引用计数更新
也就是说,很多“我只是图省事用了 shared_ptr”的代码,实际上已经把原子开销带进来了。
所以在性能敏感路径里:
- 不要把共享所有权当默认方案
19. 一个高频模式:单生产者发布结果
1 | struct Result { |
这类模式是 acquire/release 最值得优先熟练掌握的用法。
20. 一个高频模式:纯统计计数
1 | std::atomic<long long> counter{0}; |
这里若只是最后读总数,relaxed 往往就足够,因为你并不依赖这个计数去同步其他数据。
21. 常见误区
21.1 “原子 = 不需要思考同步”
错。
原子只解决了一部分问题,真正难的是:
- 你想建立什么可见性关系
- 你要不要顺序保证
21.2 “我用了原子,就能保护一大片普通变量”
只有当发布/获取关系写对时才成立。
21.3 “lock-free 一定更先进”
并发工程里先进不先进不重要,重要的是:
- 正确
- 可维护
- 有实际性能收益
21.4 “volatile 可以替代 atomic”
不能。volatile 不是线程同步原语。
22. 一页总结
原子操作最重要的不是 API 数量,而是建立这条理解链:
- 原子保证操作本身不会被数据竞争破坏
- 内存序决定不同线程看到这些操作及相关普通内存的顺序关系
relaxed只保原子性,不保发布顺序release/acquire是最常用的发布-获取模型seq_cst最直观,适合先写对- 无锁不是性能银弹,false sharing 和 CAS 重试都可能很贵
如果只记两个最高频结论:
- 纯计数:优先考虑
relaxed - 发布数据给别的线程:优先考虑
release/acquire
23. 建议继续补充的相关主题
和本篇衔接最紧密的内容:
- C++ 内存模型正式定义
- ABA 问题
- 无锁队列 / Michael-Scott Queue
- hazard pointers / epoch reclamation
atomic_ref与std::barrier
24. 参考资料
cppreference:
std::atomic
https://en.cppreference.com/w/cpp/atomic/atomiccppreference: memory order
https://en.cppreference.com/w/cpp/atomic/memory_orderC++ Core Guidelines, Concurrency
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines