你真的理解原子操作和锁吗
2025-02-26
最近重翻了《Effective Modern C++》这本书。
言简意赅,对如何高效使用 C++ 11,提供了不少具体可行的建议。
尤其是并发的部分。
Meyers 说:
优先选用基于任务而非基于线程的程序设计。
这让我想到前一篇关于 Chromium 任务的 文章🔗。
另外是关于 std::atomic
。
书里没有谈锁和条件变量,美中不足。
今天这篇文章,打算把这一块拼图补上,谈一谈原子操作(Atomic Operation)和锁(Lock),各自的概念,差异,适合的场景。
通常我们认为的原子操作更快,锁的开销很大,究竟正确吗?
1. 概念
首先要清楚,对于单线程编程,原子操作和锁都是不需要的。
只有在并发编程的时候才会用到。
熟悉并发编程的朋友应该都清楚,多个线程,同时访问同一块内存,会导致冲突。
举个通俗的例子。
两个人,面前有块黑板,黑板上有个数字。两个人的任务,都是将黑板上的数字加一后写下来。
最开始的数字是 1。
两个人同时来到黑板面前,看到数字后,心里计算了一会儿,开始写。
那么黑板上最后的数字可能是什么?
可能是 2,也可能是 3。
有可能是 2,是因为如果两人在对方写之前,看到的都是 1 这个数字,最后的结果就是 2。
解决这个问题,有两个办法,一种是将读和写操作包装成一个不可分割的整体,另外就是一个人“独占”黑板直到释放。
即原子操作和锁分别要做的事情。
在开始学习并发编程的时候,我将其误认为是一个东西。其实不是。它们底层的生效机制完全不同。
原子操作是指一项操作不能被分解和中断。像上文提到的例子,如果将读和写合并成一个操作,最后的结果只能是 3。
锁是通过阻塞其他线程,确保只有一个线程可以访问共享资源。这看上去有些霸道,不过可以解决问题:在特定时间,黑板只属于我一个人,其他人都得等着。
2. 差异
从使用上看,C++ 分别提供了 std::atomic<T>
和 std::mutex
。
各自用法如下:
// 方案 A:原子操作std::atomic<int> counter{0};void increment() { counter.fetch_add(1, std::memory_order_relaxed);}
// 方案 B:互斥锁std::mutex mtx;int counter = 0;void increment() { std::lock_guard<std::mutex> lock(mtx); ++counter;}
通常,原子操作针对一个特定对象,粒度较细;锁则是一块区域,经常是多个变量,粒度较粗。
在底层实现上,差异更大。
原子操作依赖 CPU 的 LOCK
指令前缀。
举例来说,x86 在执行 LOCK ADD
时,背后发生了:
- 使用缓存锁定(Cache Locking) 锁定缓存行
- 如果数据没有缓存或跨缓存行,总线锁定(Bus Locking)
- 操作完成后利用 MESI 协议维护缓存一致性
这里有许多背景知识需要填充。
由于 CPU 处理和内存访问的速度差异极大,因此 CPU 内部有 多级缓存🔗。多核心缓存的一致性,通过 协议🔗 协议证。

根据局部性(Locality)原理,缓存一般以行为单位组织,称为缓存行(Cache Line),主流大小是 64 Bytes。
很多时候,CPU 都是直接从缓存行里读取变量,效率更高。
如果变量在缓存行里,锁定该行,阻塞其他核对该行的操作。
如果变量不在缓存里,就要从内存中读取,此时锁定总线,阻塞其他核和内存的通信。这种方式开销较大。
最后执行缓存一致性协议,常见的有 MESI 协议🔗。
这里提一嘴 volatile
,因为 Meyers 在书中也提到了。它只是用来防止编译器优化,确保每次都从内存中读取,本身无法保证原子性。
相较于 CPU 原生支持的原子指令,锁的实现更多放在操作系统里。这里以 Linux 为例。
- 利用 Futex🔗 机制先在用户态自旋
- 若成功,直接进入临界区,不必进入内核态;失败后执行系统调用,进入内核态,在队列中等待唤醒
- 借助 优先级继承(Priority Inheritance)🔗 优先级继承(Priority Inheritance)少优先级反转的问题(对实时系统(RTOS)影响较大)
由此可见,在 Linux 平台上,如果没有竞争(Race),加减锁的开销理应很小,毕竟都是在用户态完成。
看上去平平无奇的一把锁,操作系统其实在背后做了很多优化:核心是尽可能减少进入内核态。无论是 futex 的使用,还是自旋等待,都是在拖延进入内核态的时间。
有点 惰性求值🔗 惰性求值味道。
3. 最佳实践
最后说点实用的,什么时候用原子操作,什么时候用锁。
一句话概括,简单变量,或者说简单操作,例如标志位,用原子操作;复杂变量,尤其是涉及多个变量,或操作较多,用锁。
原子操作是 CPU 天生支持,性能很好;锁的话,因为会导致线程挂起,性能略差。
当然,这只是理论上。对于实际环境中的性能测试,最好用 Profiler 验证。
DeepSeek 说,
真正的高手既不会滥用锁导致性能泥潭,也不会迷信原子操作带来隐蔽 bug。记住:没有最好的同步机制,只有最合适的场景选择。
其实最好的同步机制也是有的,就是没有同步。
就像 Chromium 搭建的那套基础设施一样。
4. 总结
看似简单的两个概念背后,涉及到硬件和操作系统的无数优化。
侯捷说:源码面前,了无秘密。
有时候,尤其是现在,光会写代码是不够的,要深入到底层,了解原理,方能以不变应万变。
最近在用 Cursor,我发现,单论写代码,恐怕是写不过它的。而且差距估计会越来愈大。
人的优势,在于对生活的好奇,对现实的抽象,对问题的系统性认识,和对未知领域的探索。
(完)
参考
- 本文作者:Plantree
- 本文链接:https://plantree.me/blog/2025/atomic-and-lock/
- 版权声明:所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
最后更新于: 2025-02-27T06:41:37+08:00