并发编程(四):互斥锁的实现——从 Runtime 到 CPU

目录


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

上一篇从语言内存模型的规则出发,看 Mutex 如何提供 Atomicity、Visibility 和 Ordering。

这一篇继续往下看:Java、Go 和 CPython 分别如何在 Runtime 和 CPU 层实现这些保证。


1. HotSpot 如何实现 synchronized?

1.1 分层

先把 Java 的实现层次固定下来:

Java Language
实例:synchronized
作用:声明同步区域
        │
        ▼
JVM Bytecode
实例:monitorenter / monitorexit(synchronized 代码块)
作用:表达 Monitor 的进入 / 退出
        │
        ▼
JVM Implementation(HotSpot)
实例:Synchronization Runtime
作用:实现 JVM 规定的 Monitor 语义
        │
        ▼
x86-64 Hardware
实例:Atomic Instruction / Cache Coherence / Fence
作用:提供 Atomicity、Visibility 与 Ordering 的硬件基础

1.2 一次 synchronized 的完整实现路径

从 Java Code 经过 JVM Bytecode、HotSpot、OS Thread 到 x86-64 Hardware 的加锁、竞争、临界区和解锁主流程。

上面的时序图只保留跨层主流程。Runtime 内部的竞争路径可以进一步展开为:

monitorenter 先尝试 Lightweight Locking Fast Path,失败后先进行 fast-lock spinning;持续竞争时再膨胀为 ObjectMonitor,并在 ObjectMonitor 内继续 CAS、Spin 和 Park。

1.3 Atomicity

Atomicity 对应上面流程图里的两次原子竞争:

Fast Path:CAS markWord lock bits [Acquire]
Inflated Monitor:CAS _owner [Acquire]

它们都在原子地修改锁状态。多个线程同时竞争时,只有一个线程能够成功获得锁并进入临界区。

1.4 Visibility 和 Ordering

Visibility 和 Ordering 对应流程图里的锁边界:

获取锁:CAS ... [Acquire]
释放锁:release lock state [Release]

Release / Acquire 把前后两个临界区连接起来,使前一个持锁者在临界区中的写入,能够被后一个持锁者按照正确的顺序观察到。

继续向下,这些保证再由 CPU 的 Cache Coherence 和内存顺序约束落实。

所以 Java 与第一篇的硬件模型对应为:

Atomicity
  → Atomic Instruction
  → HotSpot / x86-64: LOCK CMPXCHG

Visibility
  → Cache Coherence
  → 典型 x86-64 CPU:MESI-family(如 MESIF / MOESI)

Ordering
  → Fence
  → HotSpot / x86-64: LOCK ADDL $0, 0(%rsp)

这里的 LOCK ADDL $0, 0(%rsp) 对应 HotSpot 在 Linux x86 上实现 full fence 的实际路径;并不表示每次 synchronized 都会额外执行这条指令。x86 自身的内存顺序规则以及 LOCKed RMW 也会参与建立所需的顺序约束。

相关实现可以继续查看 OpenJDK 的 markWord.hppobjectMonitor.inline.hppgraphKit.cpporderAccess_linux_x86.hpp


2. Go Runtime 如何实现 sync.Mutex?

2.1 分层

Go API
实例:sync.Mutex / Lock() / Unlock()
作用:声明互斥边界
        │
        ▼
Go Implementation(sync / internal/sync)
实例:Mutex
作用:实现 sync.Mutex 语义
        │
        ▼
Go Runtime / Scheduler
实例:Semaphore
作用:处理 Goroutine 的等待 / 唤醒
        │
        ▼
x86-64 Hardware
实例:Atomic Instruction / Cache Coherence / Fence
作用:提供 Atomicity、Visibility 与 Ordering 的硬件基础

2.2 一次 sync.Mutex 的完整实现路径

Go 的 Lock 和 Unlock 如何经过 sync internal/sync、Go Runtime Scheduler 和 x86-64 Hardware 完成原子竞争、等待、唤醒和释放。

上面的时序图只保留跨层主流程。sync.Mutex 内部的竞争路径可以进一步展开为:

Lock 先尝试 Fast Path CAS,失败后进入 lockSlow,经过 Spin、状态更新和 Semaphore 等待;Unlock 原子释放锁,并在需要时唤醒等待的 Goroutine。

2.3 Atomicity

Atomicity 对应上面流程里的锁状态原子修改:

获取锁:CompareAndSwapInt32(&state, 0, mutexLocked)
释放锁:AddInt32(&state, -mutexLocked)

多个 Goroutine 同时竞争时,只有一个能够成功把 state 从 unlocked 改成 locked,并进入临界区。

在 amd64 上,这两类原子操作分别落到 LOCK CMPXCHGLLOCK XADDL

2.4 Visibility 和 Ordering

Visibility 和 Ordering 对应 Lock / Unlock 形成的同步边界。前一个持锁者完成 Unlock 后,后一个成功 Lock 的 Goroutine 能按照正确的顺序观察前一个临界区中的写入。

继续向下,这些保证由 CPU 的 Cache Coherence、x86 内存顺序以及原子指令本身的顺序约束落实。

所以 Go 与第一篇的硬件模型对应为:

Atomicity
  → Atomic Instruction
  → Go / x86-64: LOCK CMPXCHGL / LOCK XADDL

Visibility
  → Cache Coherence
  → 典型 x86-64 CPU:MESI-family(如 MESIF / MOESI)

Ordering
  → Fence / 等价顺序约束
  → Go / x86-64: LOCK CMPXCHGL / LOCK XADDL

Go 的这条 amd64 Mutex 路径不需要额外插入一条 MFENCE;LOCKed RMW 本身已经提供了这里需要的原子更新和顺序约束。

实现可以参考 internal/sync/mutex.goruntime/sema.gointernal/runtime/atomic/atomic_amd64.s


3. CPython 如何实现 threading.Lock?

这一节只讨论当前 CPython。

3.1 分层

Python API
实例:threading.Lock / acquire() / release()
作用:声明互斥边界
        │
        ▼
CPython Binding(_thread)
实例:_thread.lock
作用:将 Python Lock API 映射到 CPython 锁实现
        │
        ▼
CPython Implementation
实例:PyMutex
作用:实现 Lock 语义
        │
        ▼
x86-64 Hardware
实例:Atomic Instruction / Cache Coherence / Fence
作用:提供 Atomicity、Visibility 与 Ordering 的硬件基础

3.2 一次 threading.Lock 的完整实现路径

Python 的 Lock acquire 和 release 如何经过 _thread、CPython PyMutex、OS Thread 和 x86-64 Hardware 完成原子竞争、等待、唤醒和释放。

上面的时序图只保留跨层主流程。CPython 内部的竞争路径可以进一步展开为:

acquire 先尝试 PyMutex Fast Path,失败后在可用时短暂 Spin,再进入 Parking Lot;release 根据 waiter 状态直接解锁或唤醒等待线程。

3.3 Atomicity

Atomicity 对应 PyMutex_bits 的原子状态修改:

获取锁:_Py_atomic_compare_exchange_uint8(..., _Py_LOCKED)
释放锁(无 waiter):_Py_atomic_compare_exchange_uint8(..., _Py_UNLOCKED)

存在 waiter 时,释放路径会进入 _PyParkingLot_Unpark(),并由回调原子更新 _bits

多个线程同时竞争时,只有一个能够成功设置 _Py_LOCKED 并进入临界区。

3.4 Visibility 和 Ordering

Visibility 和 Ordering 对应 acquire() / release() 形成的同步边界。当前 CPython 的默认原子 compare-exchange / store 在 GCC、Clang 实现中使用 __ATOMIC_SEQ_CST

继续向下,这些保证由 CPU 的 Cache Coherence、x86 内存顺序以及这些顺序一致原子操作落实。

因此当前 CPython 与第一篇的硬件模型对应为:

Atomicity
  → Atomic Instruction
  → CPython / x86-64: LOCK CMPXCHGB

Visibility
  → Cache Coherence
  → 典型 x86-64 CPU:MESI-family(如 MESIF / MOESI)

Ordering
  → Fence / 等价顺序约束
  → CPython / x86-64: SEQ_CST atomic
    (典型为 LOCK CMPXCHGB / memory XCHGB)

这里同样不要求每次 Lock 都额外执行 MFENCE;在 x86-64 上,LOCKed RMW 和顺序一致原子操作本身已经提供相应的顺序约束。

实现可以参考 Modules/_threadmodule.cPython/lock.cPython/parking_lot.cInclude/cpython/pyatomic_gcc.h


4. 三种实现的共同套路

看完 Java、Go 和 CPython,再把具体实现拿掉,只看一把 Mutex 最终需要解决什么问题。

4.1 谁能进入?——原子修改锁状态

假设一把锁内部只有一个状态:

0 = unlocked
1 = locked

A 和 B 不能先分别读取 0,再同时写入 1。否则两边都会认为自己获得了锁。

因此“检查锁状态并把它改成 locked”必须是一个原子操作,例如 Compare-And-Swap:

Compare-And-Swap(lock_state, 0, 1)

两个执行单元同时竞争:

CPU A                         CPU B

CAS 0 -> 1                   CAS 0 -> 1
    │                             │
    ▼                             ▼
  success                       failed

只有一个竞争者能够成功修改锁状态。Mutex 再用这个很小的硬件原子操作,保护 counter++ 这样的任意临界区。

4.2 没抢到的人怎么办?——Spin / Park / Wakeup

CAS 失败以后,执行单元不能无休止地竞争锁状态。

常见路径是:

尝试原子获取锁
        │
        ├── 成功 ──> 进入临界区
        │
        └── 失败
              │
              ├── 短暂 Spin
              │      │
              │      └── 再次尝试
              │
              └── Park / 等待
                         │
                         └── 锁释放后被唤醒

Spin 适合等待时间很短的情况,可以避免立即进入阻塞路径;竞争持续时则需要 Park,避免一直占用 CPU。

4.3 抢到锁之后,为什么能看到前一个持锁者留下的数据?

前两个问题解决了“谁能拿到锁”和“拿不到锁怎么办”。第三个共同问题是:前一个持锁者在临界区里的写入,为什么能被后一个持锁者看到?

从三种实现的共同模型来看,关键是锁的释放和获取形成了 Release / Acquire 同步边界:

前一个持锁者
临界区写入
    │
    ▼
unlock [Release]
    │
    ▼
lock [Acquire]
    │
    ▼
后一个持锁者
临界区读取

Release 约束释放锁之前的内存操作,Acquire 约束获得锁之后的内存操作。

因此,当后一个执行单元成功获得前一个执行单元释放的同一把锁时,前一个临界区中的写入会排在后一个临界区的读取之前。对 Mutex 来说,这就是三种实现共同需要建立的内存顺序。

具体的 Runtime 和 CPU 如何落实这个 Release / Acquire 边界,前三节已经分别展开。


5. 下一篇:Atomic

Mutex 的巧妙之处在于:它只需要维护一个很小的原子状态,就能为任意大小的临界区建立互斥边界。

Atomic 则更进一步收缩了保护范围:它不再保护一整段临界区,而是直接保证一个共享变量的一次读、写或更新不可分割。

下一篇继续看这个更小的同步单元是如何实现的。


本文首发于 ThinkerQAQ 的个人博客,由作者本人同步发布。原文可能持续修订,最新版本请以个人博客为准。

posted @ 2026-09-23 21:11  ThinkerQAQ  阅读(8)  评论(0)    收藏  举报