【多线程】什么是原子操作(Atomic Operation)?
本文来自于我关于多线程系列文章。欢迎阅读、点评与交流
1.什么?就是【多线程】互斥锁(Mutex)
2.【多线程】临界区(Critical Section)是什么?
3.【多线程】计算机领域中的各种锁
4.【多线程】信号量(Semaphore)是什么?
5.【多线程】信号量(Semaphore)常见的应用场景
6.【多线程】条件变量(Condition Variable)是什么?
7.【多线程】监视器(Monitor)是什么?
8.原子操作(Atomic Operation)?就是【多线程】什么
原子管理(Atomic Operation)是一个非常核心的计算机概念,涉及到并发编程的底层基础。我们来详细拆解一下。
什么?就是1. 原子操作
简单来说,原子操作是一个不可分割的操作。在执行过程中,它要么完全执行成功,要么完全没执行,不会出现执行到一半被中断的情况,外界也看不到中间状态。
你可以把它想象成一个“瞬间完成”的操作。
一个经典的非原子操作例子:i++
这个看似简单的语句,在CPU层面通常需要三个步骤:
- 读取:从内存中读取变量
i
的值到寄存器。 - 修改:在寄存器中将值加一。
- 写入:将新值写回内存。
在单线程环境下,这没问题。但在多线程或多核环境下,障碍就来了:
- 线程 A 读取
i
的值为 10。 - 线程 A 将值加一,变成 11(还在寄存器中,未写回)。
- 此时,线程 B 被调度,它读取
i
的值,仍然是 10。 - 线程 B 将值加一,变成 11(寄存器中)。
- 线程 B 将 11 写回内存。
- 线程 A 被重新调度,也将 11 写回内存。
最终结果 i
等于 11,但两个线程各执行了一次 i++
,我们期望的结果应该是 12。这就是因为 i++
不是原子操作,导致了数据竞争。
原子操作的例子:使用原子加执行。
如果 fetch_add
是一个原子操作,那么整个“读取-修改-写入”序列会被捆绑成一个不可分割的单元。
- 线程 A 开始执行原子加操控。它会“锁定”这个变量(注意,这里的锁定是硬件层面的机制,不是软件互斥锁)。
- 在线程 A 达成整个操作(读取、加一、写入)之前,线程 B 无法访问或修改这个变量。
- 线程 A 完成后,
i
变为 11。 - 11,加一后变为 12 并写回。就是线程 B 开始执行原子加操作,读取到的值
最终结果 i
等于 12,符合预期。
原子操作的核心特性:
- 不可分割性:操作要么全做,要么不做。
- 顺序性内存屏障的一种,能保证指令执行顺序,避免编译器或CPU的指令重排优化带来问题。就是:原子操作本身
2. 底层是如何建立的?
原子操作的实现不依赖于操作系统或编程语言提供的锁(如 mutex
),因为锁本身的实现又需要原子操作。这是一个“先有鸡还是先有蛋”的问题。原子操作的基石是硬件支持,主要是CPU提供的指令。
实现方式可以分为两大类:
方式一:单核处理器
在单核CPU上,实现原子操作相对简单。因为同一时刻只有一个线程在执行(宏观并行,微观串行)。只需要保证在执行原子管理的指令序列期间不被中断即可。
- 实现方法:在执行关键的几条指令之前关闭中断,执行完毕后再打开中断。这样,操作系统就无法通过时钟中断来剥夺当前CPU的执行权,从而不会发生线程/进程切换,保证了操作的原子性。
方式二:多核处理器
这是现代计算机的常态,也是复杂性的来源。关闭一个核心的中断无法阻止其他核心同时访问同一块内存。
核心机制:基于总线锁和缓存一致性协议(MESI)。
1. 总线锁(Bus Locking) - 古老而管用的方法
- 原理:CPU在执行原子操作时,会发出一个
LOCK#
信号。这个信号会锁住整个内存总线(或后来的缓存总线)。 - 效果:在锁住总线期间,其他所有处理器(核心)都无法通过总线访问内存。这就强制实现了原子性,因为只有当前处理器能访问内存。
- 缺点:开销巨大。它阻止了所有其他处理器访问任何内存,即使它们访问的是无关的内存地址,严重影响了系统性能。这是一种“伤及无辜”的粗粒度锁。
2. 缓存锁(Cache Locking) - 现代主流方法
基础:缓存一致性协议(MESI)
现代CPU每个核心都有自己的高速缓存。为了保持多个缓存中数据的一致性,CPU实现了MESI这样的协议。MESI代表了缓存行的四种状态:- M(Modified,修改):缓存行是脏的,与主内存不一致,只存在于当前缓存中。
- E(Exclusive,独占):缓存行是干净的,与主内存一致,只存在于当前缓存中。
- S(Shared,共享):缓存行是干净的,与主内存一致,可能存在于多个缓存中。
- I(Invalid,无效):缓存行中的数据是无效的。
原理:当某个CPU核心要对一个变量进行原子执行时:
- 如果该变量所在的缓存行处于E(独占) 或 M(修改)状态,说明当前核心是唯一持有该缓存行最新数据的核心。
- 此时,CPU核心可以直接在它的缓存上执行原子操作(如
CMPXCHG
,比较并交换),而无需声明总线锁。操作完成后,缓存行变为M状态。 - 假设变量处于S(共享)状态,说明其他核心也可能有这份数据的副本。
- 当前核心会通过缓存一致性协议,向所有其他核心发送一个“RFO(Request For Ownership)” 请求。
- 这个请求会强制其他核心将它们对应的缓存行标记为I(无效)。
- 当当前核心收到所有响应后,它就获得了对该缓存行的独占权,然后就行安全地执行原子操作了。
整个总线。粒度更细,性能更高。就是这个过程,从效果上看,相当于“锁定”了特定内存地址所在的缓存行,而不
具体的原子指令:
CPU提供了一些直接支持原子操作的机器指令,最常见的包括:
- CAS(Compare-And-Swap,比较并交换):这是实现各种高级同步原语(如锁、无锁数据结构)的基石。它会先比较某个内存位置的值是否与预期值相等,如果相等,则存入新值。
- LL/SC(Load-Link / Store-Conditional,链接加载/条件存储):这是一对指令。
LL
从内存加载值,SC
会尝试向同一地址写入新值,但只有在从LL
到SC
期间该地址未被其他写入修改过时,写入才会成功。
编程语言(如C++的 std::atomic
,Java的 java.util.concurrent.atomic
包)和操作系统API最终都会将这些原子指令封装成易用的函数供开发者调用。
总结
特性 | 原子操作 | 互斥锁(Mutex) |
---|---|---|
实现层级 | 硬件指令(CPU) | 操作系统内核(软件) |
粒度 | 很细,针对单个内存地址 | 较粗,保护一个临界区代码 |
开销 | 非常低,通常由缓存一致性协议保证 | 较高,需要陷入内核态,可能引起线程挂起和调度 |
应用场景 | 容易的计数器、标志位、无锁数据结构 | 保护复杂的逻辑代码块或数据结构 |
关系 | 是实现互斥锁的基础 | 在原子运行(如CAS)之上搭建 |
简单来说,原子操控是CPU提供的一种硬件能力,通过总线锁或更高效的缓存锁(基于MESI协议)来保证对单一内存单元的“读-改-写”操作不可分割。它是构建一切高级并发控制工具的基石。