并发编程(五):Atomic——语言层的原子性、可见性与有序性
继续使用 counter 与 ready,从语言内存模型和公开 API 规则理解 Java AtomicInteger、Go sync/atomic 与 CPython 的 Atomic 语义边界。

目录
0. 从上一篇继续
这一篇直接从语言内存模型层的规则讨论 Atomic。
1. 三种语言的 Atomic 规则分别保证什么?
1.1 Java:AtomicInteger
先看规则。
AtomicInteger.incrementAndGet() 原文:
“Atomically increments the current value, with memory effects as specified by
VarHandle.getAndAdd.”
这条规则对应 Atomicity。读取旧值、加一、写回作为一次不可分割的 Atomic RMW 完成。
对于 Visibility 和 Ordering,AtomicInteger 的普通读写具有 volatile 语义:get() 对应 VarHandle.getVolatile,set() 对应 VarHandle.setVolatile。
JLS §17.4.4 Synchronization Order 对 volatile 的规则是:
“A write to a volatile variable v synchronizes-with all subsequent reads of v by any thread.”
因此,一个线程对 Atomic 变量的 volatile 写,可以与另一个线程后续观察到该写入的 volatile 读建立 synchronizes-with 关系。
JLS §17.4.5 Happens-before Order 还规定:
“If one action happens-before another, then the first is visible to and ordered before the second.”
这条 happens-before 关系同时给出 Visibility 和 Ordering:前面的写入不仅对后面的读取可见,而且必须按照 happens-before 规定的顺序被观察。
还是用前面的两个例子来看。
Atomicity
前面的普通版本:
Thread A Thread B
count++ count++
可能因为两个线程都读到旧值 0,最终得到 count = 1。
现在把同一个更新改成 Atomic:
AtomicInteger count = new AtomicInteger(0);
// Thread A
count.incrementAndGet();
// Thread B
count.incrementAndGet();
两次更新会依次作用在同一个值上:
Thread A Thread B
incrementAndGet() incrementAndGet()
│ │
▼ ▼
0 → 1 1 → 2
最终:
count = 2
这里 Atomicity 来自 incrementAndGet() 本身就是一次原子 RMW,而不是像 Mutex 那样把整个临界区串行化。
Visibility / Ordering
再看同一个 counter / ready 例子。普通变量版本的问题是:B 即使看到 ready == true,也不能仅靠普通读写推出前面的 counter = 1 已经对它可见。
现在只把 ready 改成 Atomic:
int counter = 0;
AtomicInteger ready = new AtomicInteger(0);
// Thread A
counter = 1; // A1
ready.set(1); // A2
// Thread B
if (ready.get() == 1) { // B1
System.out.println(counter); // B2
}
如果 B 的 ready.get() 观察到 A 的 ready.set(1),那么:
Thread A Thread B
A1 counter = 1
│
│ program order
▼
A2 ready.set(1)
│
│ synchronizes-with
▼
B1 ready.get() == 1
│
│ program order
▼
B2 read counter
通过 happens-before 的传递性:
A1 happens-before A2
A2 happens-before B1
B1 happens-before B2
↓
A1 happens-before B2
所以当 B 已经通过 Atomic ready 观察到 ready == 1 时,前面的 counter = 1 对 B 可见,并且不能再出现:
ready == 1
counter == 0
这就是 Atomic 在这个例子里提供的 Visibility 和 Ordering。
1.2 Go:sync/atomic
先看规则。
atomic.Int64.Add 原文:
“Add atomically adds delta to x and returns the new value.”
这条规则对应 Atomicity:Add(1) 把读取、加一和写回作为一次原子更新。
对于 Visibility 和 Ordering,The Go Memory Model - Atomic Values 规定:
“If the effect of an atomic operation A is observed by atomic operation B, then A is synchronized before B.”
也就是说,如果 B 的 Atomic 操作观察到了 A 的 Atomic 更新,那么 A 和 B 之间建立 synchronized-before。
同一节还规定,所有 Atomic 操作表现得像处在:
“some sequentially consistent order”
而 Go Memory Model 把 happens-before 定义为 sequenced-before 和 synchronized-before 的传递闭包。
因此,Atomic 操作不仅提供原子更新;当一个 Atomic 操作观察到另一个 Atomic 操作的结果时,也可以把前后的普通内存操作串成 happens-before 链,从而建立 Visibility 和 Ordering。
还是用同样的两个例子。
Atomicity
普通的 count++ 可能发生丢失更新。现在改成:
var count atomic.Int64
// Goroutine A
count.Add(1)
// Goroutine B
count.Add(1)
两次更新不会丢掉其中一次:
Goroutine A Goroutine B
Add(1) Add(1)
│ │
▼ ▼
0 → 1 1 → 2
最终:
count = 2
这里 Atomicity 来自 Add(1) 本身的原子更新语义。
Visibility / Ordering
再看同一个 counter / ready 例子,只把 ready 改成 Atomic:
var counter int
var ready atomic.Bool
// Goroutine A
counter = 1 // A1
ready.Store(true) // A2
// Goroutine B
if ready.Load() { // B1
fmt.Println(counter) // B2
}
如果 B 的 ready.Load() 观察到 A 的 ready.Store(true),那么:
Goroutine A Goroutine B
A1 counter = 1
│
│ sequenced-before
▼
A2 ready.Store(true)
│
│ synchronized-before
▼
B1 ready.Load() == true
│
│ sequenced-before
▼
B2 read counter
于是:
A1 happens-before A2
A2 happens-before B1
B1 happens-before B2
↓
A1 happens-before B2
所以当 B 已经通过 Atomic ready 观察到 ready == true 时,前面的 counter = 1 对 B 可见,并且不能再出现:
ready == true
counter == 0
这就是 Go Atomic 在这个例子里提供的 Visibility 和 Ordering。
1.3 CPython:应用层没有对称的 Atomic API
Python 标准库目前没有与 Java AtomicInteger、Go atomic.Int64 对称的通用整数 Atomic API。
因此,Python 应用代码没有一组可以像 Java、Go 那样直接引用的 Atomic 规则。普通的 counter += 1 不是 Python 语言层定义的 Atomic API;也不能因为某个 CPython 配置存在 GIL,或者 Runtime 内部使用 _Py_atomic_*,就把它当作应用层可以依赖的跨实现 Atomicity、Visibility 或 Ordering 保证。
CPython Runtime 确实需要 Atomic 来同步自己的共享状态。它如何把 _Py_atomic_* 落到编译器和 CPU,是下一篇的实现问题,而不是 Python 应用层公开语义的一部分。
2. 下一篇:Atomic 是怎么实现的?
这一篇停在语言层:Java 和 Go 分别通过公开 Atomic API 与内存模型定义 Atomicity、Visibility 和 Ordering;Python 应用层则没有对称的通用 Atomic API。
下一篇会把这些语言层规则转换成实现层真正需要解决的问题,分别沿着 Java AtomicInteger、Go sync/atomic 和 CPython Runtime 内部的 _Py_atomic_*,从 Runtime 一直看到 CPU。
讨论
使用 GitHub 账号参与讨论,评论会保存在 GitHub Issues 中。在 GitHub 查看