并发编程(六):Atomic 的实现——从 Runtime 到 CPU

沿着 Java AtomicInteger、Go sync/atomic 和 CPython 内部原子操作的实现路径,理解 Atomic RMW 如何落到编译器、Runtime 与 CPU。

中文发布于

目录


0. 这一篇继续回答什么?

上一篇讨论了 Atomic 在语言层提供的原子性、可见性和有序性。这一篇继续向下看:这些保证如何经过编译器和 Runtime,最终落到 CPU。

仍然只使用前面的两个例子:

counter++

以及:

counter = 1
ready = true

实现路径可以概括为:

 API  / Runtime
        ↓
CPU Atomic Instruction / Memory Ordering

1. Atomic 需要哪些底层能力?

1.1 Atomic RMW:直接完成一次更新

前面已经把“读取—修改—写回”简称为 RMW。普通的 counter++ 包含多个步骤,执行过程可能与另一边交错。

Atomic RMW 把一次更新作为不可分割的操作完成:

CPU A                       CPU B

Atomic Add counter, 1       Atomic Add counter, 1
        │                           │
        ▼                           ▼
      0 -> 1                       1 -> 2

注意Atomic RMW 是一类操作,包括:

Fetch-And-Add
Exchange
Compare-And-Swap
Atomic Bitwise Operation

CAS 只是其中一种。

1.2 CAS:当前值符合预期时才更新

CAS 接收内存地址、期望值和新值:

CAS(address, expected, newValue)

它原子地完成比较和条件更新:

 expected
   /             \
                
 │                 │
 ▼                 ▼
 newValue      
            

CAS 可以构造需要根据旧值计算新值的更新:

loop:
    old = atomic_load(counter)
    new = old + 1

    if CAS(counter, old, new):
        break

但这不是 Atomic Add 的唯一实现方式。具体使用原子加法还是 CAS,由语言 API、编译器和目标架构决定。

1.3 Memory Ordering:限制编译器和 CPU 重排

原子地修改 ready 只解决了这一次读写本身不能交错。要让它发布前面的 counter,还需要相应的内存顺序:

 A                   B

counter = 1
AtomicStore(ready, true)

                            if AtomicLoad(ready) {
                                print(counter)
                            }

实现需要同时处理两层约束:




        +

CPU
使

不同语言对 Atomic 操作规定的内存顺序不同,底层生成的指令也不必完全相同。


2. Java:从 AtomicInteger 到 CPU

2.1 AtomicInteger 与 HotSpot Intrinsic

Java 的原子加法从:

AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();

开始。当前实现可以沿着下面的路径理解:

AtomicInteger.incrementAndGet()
        ↓
Unsafe / VarHandle Atomic Operation
        ↓
HotSpot Intrinsic
        ↓
 CPU  Atomic RMW

HotSpot 可以把这些操作识别为 Intrinsic,直接生成目标架构支持的原子更新,而不是一定在 Java 层执行 CAS Loop。

compareAndSet() 则沿着对应的 CAS Intrinsic 落到目标 CPU:

AtomicInteger.compareAndSet()
        ↓
HotSpot CAS Intrinsic
        ↓
CPU Compare-And-Swap

2.2 x86-64 上的原子加法和 CAS

固定到 x86-64,原子加法可以简化理解为:

mov  eax, 1
lock xadd dword ptr [counter], eax

XADD 读取旧值并写入 old + 1LOCK 前缀让整个 RMW 对其他 Core 表现为不可分割。

CAS 可以简化理解为:

; EAX 保存 expected
; ECX 保存 newValue
lock cmpxchg dword ptr [counter], ecx
sete al

这些是等价的简化形式,不代表某个具体 JVM 版本生成的完整指令序列。

对于 counter + ready,HotSpot 还要按照 JMM 和具体 Atomic 方法的内存效果限制编译期重排,并把这些约束映射到 x86-64 的内存顺序能力。不能只看到 LOCK 指令,就忽略编译器这一层。

2.3 从 Runtime 到 CPU:Java 实现总结

AtomicInteger.incrementAndGet / compareAndSet / get
        ↓ AtomicInteger / Unsafe / VarHandle
HotSpot C2 Intrinsic 
        ↓ x86-64
LOCK XADD / LOCK CMPXCHG / MOV

incrementAndGet() 经过 Unsafe.getAndAddInt() 被 HotSpot 识别为 Intrinsic,在 x86-64 上可以生成 lock xaddcompareAndSet() 可以生成 lock cmpxchgget() 则按 volatile 读处理。带 LOCK 的 RMW 指令保证单次更新不可分割,HotSpot 按照这些方法的内存效果限制编译器重排,再结合 x86-64 的内存顺序保证跨线程的可见性和顺序。因此,当 B 的 get() 观察到 A 对 counter 的更新时,也能看到 A 在更新前写入的 ready


3. Go:从 sync/atomic 到 CPU

3.1 atomic.Int64 与 internal/runtime/atomic

Go 的原子加法从:

var counter atomic.Int64
counter.Add(1)

开始。实现路径可以概括为:

atomic.Int64.Add
        ↓
sync/atomic
        ↓
internal/runtime/atomic
        ↓
 Atomic RMW

CAS 的路径类似:

atomic.Int64.CompareAndSwap
        ↓
internal/runtime/atomic
        ↓
CPU Compare-And-Swap

Go Memory Model 规定 Atomic 操作的可观察结果,编译器和架构实现不能生成违反这些规则的代码。

3.2 amd64 上的原子加法和 CAS

在 amd64 上,原子加法可以落到带 LOCK 前缀的 XADD

LOCK
XADDQ AX, 0(BX)

CAS 可以落到:

LOCK
CMPXCHGQ CX, 0(BX)

因此 counter.Add(1) 可以直接使用硬件原子加法,不需要先写成 CAS Loop。

对于 counter + ready,编译器还要保留 Go Memory Model 对 Atomic 操作规定的顺序关系。amd64 的实现选择了强内存顺序的原子指令;其他架构可以使用不同的指令和屏障组合。

3.3 从 Runtime 到 CPU:Go 实现总结

atomic.Int64.Add / CompareAndSwap / Load
        ↓ sync/atomic
internal/runtime/atomic /  Atomic 
        ↓ amd64 
LOCK XADD / LOCK CMPXCHG / MOV

atomic.Int64.Add() 经过 sync/atomic 和编译器、Runtime 的 Atomic 操作,在 amd64 上可以生成 LOCK XADDCompareAndSwap() 可以生成 LOCK CMPXCHGLoad() 则使用原子 Load。带 LOCK 的指令完成不可分割的 RMW,编译器同时保留 Go Memory Model 规定的顺序关系。因此,如果 B 的 Load() 观察到 A 的 Add(),两者之间就建立 synchronized-before,A 在更新前写入的 ready 也必须对 B 可见。


4. CPython:Runtime 内部的 Atomic

Python 标准库没有与 Java AtomicInteger、Go atomic.Int64 对称的通用整数 Atomic API。本节讨论的是 CPython Runtime 内部实现,不把它写成普通 Python 程序可以直接依赖的接口。

4.1 _Py_atomic 与编译器 Builtin

CPython 内部使用:

_Py_atomic_add_*
_Py_atomic_compare_exchange_*
_Py_atomic_exchange_*
_Py_atomic_load_*
_Py_atomic_store_*

在 GCC / Clang 环境下,这些封装会继续使用编译器 Atomic Builtin:

__atomic_fetch_add
__atomic_compare_exchange_n
__atomic_load_n
__atomic_store_n

实现路径是:

CPython Runtime
        ↓
_Py_atomic_*
        ↓
GCC / Clang Atomic Builtin
        ↓
 CPU 

4.2 Linux x86-64 上的实现

在 Linux x86-64 + GCC / Clang 下,原子加法通常可以生成 LOCK XADD,CAS 通常可以生成 LOCK CMPXCHG

_Py_atomic_add_*
        ↓
__atomic_fetch_add
        ↓
LOCK XADD
_Py_atomic_compare_exchange_*
        ↓
__atomic_compare_exchange_n
        ↓
LOCK CMPXCHG

具体指令取决于编译器版本、目标架构、操作宽度和内存顺序参数,所以这里只说明典型路径。

4.3 从 Runtime 到 CPU:CPython 实现总结

CPython Runtime 
        ↓ _Py_atomic_*
GCC / Clang __atomic Builtin
        ↓ Linux x86-64
LOCK XADD / LOCK CMPXCHG / MOV 

CPython Runtime 先通过 _Py_atomic_add_*_Py_atomic_compare_exchange_*_Py_atomic_load_* 等内部接口表达原子操作和所需的内存顺序,再由 GCC / Clang 的 __atomic Builtin 生成目标代码。在 Linux x86-64 上,原子加法通常落到 LOCK XADD,CAS 通常落到 LOCK CMPXCHG,Load / Store 和必要的顺序约束则按具体内存顺序参数生成。原子指令保证 Runtime 状态更新不可分割,编译器和 CPU 按参数维持可见性与顺序;但整条路径只服务于 CPython 内部,不是普通 Python 代码可以调用的 Atomic API。


5. Atomic 与 CAS 的代价

前面已经把 Atomic 一路追到 CPU。接下来再看它的代价:竞争变多以后,CAS 可能反复重试,同一条 Cache Line 会在多个 Core 之间来回转移,还可能遇到 ABA。

5.1 CAS 失败需要重试

低竞争时,一个 CAS Loop 可能一次成功:

read old
CAS success

高竞争时,多个执行单元可能同时读取同一个旧值:

Thread A        Thread B        Thread C

read 10         read 10         read 10
CAS success     CAS failed      CAS failed
                retry           retry

失败者需要重新读取、计算和尝试。Atomic 避免进入 Mutex 的 Park / Wakeup 路径,不代表竞争没有成本。

5.2 Cache Line 竞争仍然存在

多个 Core 反复修改同一个 counter 时,相关 Cache Line 的修改权仍然要在 Core 之间转移:

Core A  counter
        ↓
 Cache Line 
        ↓
Core B  counter
        ↓

Atomic 去掉了应用层临界区,但没有去掉共享内存竞争。高竞争计数场景可能需要分散热点的设计,不能只比较 Atomic 和 Mutex 的 API 长短。

5.3 ABA:值相同不代表没有变化

CAS 只比较当前值是否仍等于 expected。

假设执行单元 A 读到:

counter = 1

随后执行单元 B 完成:

1 -> 2 -> 1

A 恢复后执行:

CAS(counter, 1, 3)

CAS 会成功,因为当前值又变回了 1,但中间确实发生过变化。这就是 ABA:

一种处理方式是把值和版本一起比较:

(1, version=1)
        ↓
(2, version=2)
        ↓
(1, version=3)

Java 已经把这种做法封装成 AtomicStampedReference:CAS 会同时比较引用和版本号。即使引用从 A 变成 B 后又变回 A,版本号也已经改变,因此不会被误认为“期间没有变化”。

Go 的 sync/atomic 没有直接对应的 Stamped Reference。如果值和版本号可以放进同一个 uint64,可以把两者编码在一起,再用 CompareAndSwap 整体更新;无法放进一个原子变量时,就需要用 Mutex 保护这两个值。

Python 应用层没有公共 CAS API,也没有与 AtomicStampedReference 对称的类型。CPython Runtime 如果使用 CAS 实现具体算法,需要在自己的数据结构中处理 ABA,不能给普通 Python 代码提供一套通用写法。


6. 三种实现放在一起看

Java Go CPython
上层入口 AtomicInteger sync/atomic Runtime 内部 _Py_atomic_*
中间层 Unsafe / VarHandle、HotSpot Intrinsic internal/runtime/atomic GCC / Clang Atomic Builtin
x86-64 原子加法 典型为 LOCK XADD LOCK XADD 典型为 LOCK XADD
x86-64 CAS 典型为 LOCK CMPXCHG LOCK CMPXCHG 典型为 LOCK CMPXCHG
应用层公共 API 没有对称的通用整数 Atomic API

三种实现最终使用的是同类硬件能力,但语言 API、规范来源和 Runtime 路径并不相同。


7. 下一篇:volatile

Atomic 解决的是 counter++ 这类 RMW 的原子性问题。如果不需要原子更新,只需要通过 ready 让另一个线程看到已经写完的 counter,在 Java 中可以使用 volatile。下一篇继续说明它如何保证 Visibility 和 Ordering,同时说明 Go 和 Python 没有对应的 volatile 关键字。

讨论

使用 GitHub 账号参与讨论,评论会保存在 GitHub Issues 中。在 GitHub 查看