ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

并发编程(六):Atomic 的实现——从 Runtime 到 CPU 目录0. 这一篇继续回答什么1. HotSpot 如何实现 AtomicInteger1.1 分层1.2 一次 AtomicInteger 的完整实现路径1.3 Atomicity1.4 Visibility 和 Ordering2. Go Runtime 如何实现 sync/atomic2.1 分层2.2 一次 atomic.Int64 的完整实现路径2.3 Atomicity2.4 Visibility 和 Ordering3. CPython 如何实现内部 Atomic3.1 分层3.2 一次 CPython Atomic 的完整实现路径3.3 Atomicity3.4 Visibility 和 Ordering4. 三种实现的共同套路4.1 一次更新怎么做到不可分割4.2 同时更新冲突了怎么办4.3 为什么还能保证 Visibility 和 Ordering5. Atomic 和 Mutex 有什么区别5.1 一次状态更新两种做法5.2 多个 Atomic 为什么不能代替一把 Mutex5.3 怎么选6. 下一篇volatile0. 这一篇继续回答什么上一篇从语言内存模型的规则出发看 Atomic 如何提供 Atomicity、Visibility 和 Ordering。这一篇继续往下看Java、Go 和 CPython 分别如何在 Runtime 和 CPU 层实现这些保证。1. HotSpot 如何实现 AtomicInteger1.1 分层先把 Java 的实现层次固定下来JDK API 实例AtomicInteger 作用提供 Atomic 操作 │ ▼ JDK Internal 实例Unsafe 作用提供底层 Atomic Primitive │ ▼ JVM ImplementationHotSpot 实例Intrinsic / C1 / C2 作用把 Atomic Primitive 降低到机器指令 │ ▼ x86-64 Hardware 实例Atomic Instruction / Cache Coherence / Fence 作用提供 Atomicity、Visibility 与 Ordering 的硬件基础1.2 一次 AtomicInteger 的完整实现路径上面的时序图只保留跨层主流程。HotSpot 内部的核心路径可以进一步展开为1.3 AtomicityAtomicity 对应上面流程图里的两种原子更新Atomic AddLOCK XADDL CAS LOCK CMPXCHGL它们都把一次共享状态修改作为不可分割的 Atomic RMW 完成。1.4 Visibility 和 OrderingVisibility 和 Ordering 对应上面流程图里的 Atomic 写入与读取边界写侧Atomic Add / CAS 读侧Atomic Load上层的 JMM / VarHandle Memory Effects 规定这些操作的内存语义HotSpot 在编译时保留相应的顺序约束再由 CPU 的 Cache Coherence 和内存顺序落实。所以 Java 与第一篇的硬件模型对应为Atomicity → Atomic Instruction → HotSpot / x86-64: LOCK XADD / LOCK CMPXCHG Visibility → Cache Coherence → 典型 x86-64 CPUMESI-family如 MESIF / MOESI Ordering → Compiler Ordering Hardware Memory Ordering → HotSpot x86-64 ordering rules实现可以继续查看 OpenJDK 的AtomicInteger.java、Unsafe.java、vmIntrinsics.hpp、c1_GraphBuilder.cpp和c2compiler.cpp。2. Go Runtime 如何实现 sync/atomic2.1 分层Go API 实例sync/atomic / atomic.Int64 作用声明 Atomic 操作 │ ▼ Go Implementationsync/atomic 实例AddInt64 / CompareAndSwapInt64 / LoadInt64 作用实现 Atomic API 语义 │ ▼ Go Runtime 实例internal/runtime/atomic 作用实现底层 Atomic Primitive │ ▼ x86-64 Hardware 实例Atomic Instruction / Cache Coherence / Fence 作用提供 Atomicity、Visibility 与 Ordering 的硬件基础2.2 一次 atomic.Int64 的完整实现路径上面的时序图只保留跨层主流程。Go 内部的核心路径可以进一步展开为2.3 AtomicityAtomicity 对应上面流程图里的两种原子更新Atomic AddLOCK XADDQ CAS LOCK CMPXCHGQ它们都把一次共享状态修改作为不可分割的 Atomic RMW 完成。2.4 Visibility 和 OrderingVisibility 和 Ordering 对应上面流程图里的 Atomic 写入与读取边界写侧Atomic Add / CAS 读侧Atomic LoadGo Memory Model 规定 Atomic 操作的同步与顺序语义编译器和 Runtime 将这些语义保留到目标架构再由 CPU 的 Cache Coherence 和内存顺序落实。所以 Go 与第一篇的硬件模型对应为Atomicity → Atomic Instruction → Go / x86-64: LOCK XADDQ / LOCK CMPXCHGQ Visibility → Cache Coherence → 典型 x86-64 CPUMESI-family如 MESIF / MOESI Ordering → Compiler / Runtime Ordering Hardware Memory Ordering → Go Atomic semantics x86-64 ordering rules实现可以继续查看sync/atomic/type.go、sync/atomic/asm.s和internal/runtime/atomic/atomic_amd64.s。3. CPython 如何实现内部 AtomicPython 标准库没有与AtomicInteger、atomic.Int64对称的通用整数 Atomic API所以这一节从 CPython Runtime 内部开始。3.1 分层CPython Runtime 实例Runtime 内部共享状态 作用调用内部 Atomic 操作 │ ▼ CPython Implementation 实例_Py_atomic_* 作用实现 Runtime Atomic 语义 │ ▼ C Compiler 实例GCC / Clang __atomic_* 作用把 Atomic 与 Memory Order 映射到目标架构 │ ▼ x86-64 Hardware 实例Atomic Instruction / Cache Coherence / Fence 作用提供 Atomicity、Visibility 与 Ordering 的硬件基础3.2 一次 CPython Atomic 的完整实现路径上面的时序图只保留跨层主流程。CPython 内部的核心路径可以进一步展开为3.3 AtomicityAtomicity 对应上面流程图里的两种原子更新Atomic Add_Py_atomic_add_* → __atomic_fetch_add → Atomic RMW CAS _Py_atomic_compare_exchange_* → __atomic_compare_exchange_n → Atomic CAS在 x86-64 上编译器通常会把这两类操作分别落实为LOCK XADD和LOCK CMPXCHG一类的原子指令。3.4 Visibility 和 OrderingVisibility 和 Ordering 对应上面流程图里的 Atomic 写入与读取边界写侧Atomic Add / CAS 读侧Atomic Load_Py_atomic_*将 Memory Order 传递给 GCC / Clang 的__atomic_*builtins再由编译器映射到目标架构的顺序约束与 Atomic 操作。所以 CPython 与第一篇的硬件模型对应为Atomicity → Atomic Instruction → x86-64: LOCK XADD / LOCK CMPXCHG Visibility → Cache Coherence → 典型 x86-64 CPUMESI-family如 MESIF / MOESI Ordering → Compiler Atomic Memory Order Hardware Memory Ordering → __atomic_* order x86-64 ordering rules这里描述的是 CPython Runtime 的实现不把它提升成 Python 应用层的 Memory Model。实现可以继续查看pyatomic.h和pyatomic_gcc.h。4. 三种实现的共同套路看完 Java、Go 和 CPython把具体 API 拿掉Atomic 做的事情其实很固定。4.1 一次更新怎么做到不可分割普通的 read-modify-write 是三步Load ↓ Modify ↓ Store问题是其他执行单元可以插进来CPU A CPU B Load 0 Load 0 Add 1 Add 1 Store 1 Store 1Atomic 做的事情就是把这三步收成一次操作Atomic RMW │ ├── Add ├── Swap └── CAS继续往下最终由 CPU 的原子指令完成。4.2 同时更新冲突了怎么办如果是 Atomic Add两次更新都要完成counter 0 CPU A CPU B Atomic Add 0 → 1 Atomic Add 1 → 2先后顺序可以不同但不会丢掉其中一次更新。如果是 CAS情况不同CPU A CPU B CAS 0 → 1 CAS 0 → 1 │ │ ▼ ▼ success failedCAS 失败以后上层代码决定怎么办CAS │ ├── success ── done │ └── failed │ ├── return false │ └── reload → retry所以 Atomic 没有 Mutex 那条固定的Spin → Park → Wakeup路径。Add 直接完成一次原子更新CAS 失败则返回失败是否重试由上层决定。4.3 为什么还能保证 Visibility 和 OrderingAtomicity 只保证“这一笔更新不能被拆开”。Atomic 前后的普通读写还要满足上层规定的内存顺序前面的普通写入 │ ▼ Atomic Update │ ▼ Atomic Read │ ▼ 后面的普通读取这条关系继续往下由三层保证Language / Runtime Memory Semantics ↓ Compiler Ordering ↓ Hardware Memory Ordering Cache Coherence所以三种实现虽然 API 不同最终都在解决同三件事一次更新不可分割 冲突时怎么处理 前后的内存访问不能乱5. Atomic 和 Mutex 有什么区别最简单的区别只有一句Atomic 处理一次共享状态操作Mutex 保护一段代码。5.1 一次状态更新两种做法比如只想把一个计数器加一。MutexLock ↓ counter ↓ UnlockAtomicAtomic Add(counter, 1)Mutex 的思路是先让一个执行单元进入 ↓ 再执行这段代码 ↓ 最后退出Atomic 的思路是这一次更新本身 直接不可分割如果问题真的只有一次 Add、CAS、Swap 或 Load / StoreAtomic 不需要再包一层临界区。5.2 多个 Atomic 为什么不能代替一把 Mutex假设一次操作要同时改两个状态balance - 100 count把两个变量分别做成 AtomicAtomic balance - 100 ↓ ← 其他线程可能在这里读到中间状态 ↓ Atomic count只能保证两次更新各自不可分割不能保证它们合起来也是一个整体。Mutex 可以直接把两步包起来Lock ↓ balance - 100 count ↓ Unlock在这段临界区结束以前其他竞争者不能进入同一段受保护代码。所以问题一旦从“改一个状态”变成“这几步必须一起完成”Mutex 就更容易表达。5.3 怎么选直接看你要保护什么一次状态操作 Add / CAS / Swap / Load / Store ↓ Atomic一段逻辑 读取 → 判断 → 修改多个状态 ↓ Mutex两者也不是完全独立的。Mutex 自己在实现“谁先拿到锁”时底层通常就会使用 Atomic / CASAtomic / CAS ↓ 竞争锁状态 ↓ Mutex ↓ 保护临界区所以可以把关系理解成Atomic解决一次共享状态更新 Mutex在 Atomic 等底层能力之上再提供一整段临界区6. 下一篇volatileAtomic 解决的是counter这类 RMW 的原子更新。如果不需要 RMW只需要让一个线程的写入对另一个线程可见并建立正确顺序就进入了volatile的问题。下一篇继续从语言规则开始讨论 Visibility 和 Ordering。本文首发于 ThinkerQAQ 的个人博客由作者本人同步发布。原文可能持续修订最新版本请以个人博客为准。
返回列表