并发编程(十):BlockingQueue 与 Channel 的实现——从 Runtime 到 CPU

目录


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

上一篇从语言和标准库规则出发,看 BlockingQueue、Channel 和 queue.Queue 如何提供 Atomicity、Visibility 和 Ordering。

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


1. JDK 如何实现 ArrayBlockingQueue?

1.1 分层

Java Source Code
实例:BlockingQueue.put() / take()
作用:声明消息交接与阻塞等待
        │
        ▼
JDK Class Library
实例:ArrayBlockingQueue / ReentrantLock / AQS / ConditionObject
作用:维护队列状态、互斥锁和条件等待队列
        │
        ▼
JVM Implementation(HotSpot)
实例:Unsafe.park / HotSpot Parker、Continuation
作用:提供底层线程等待与虚拟线程执行机制
        │
        ▼
OS Thread / Thread Parking
实例:Platform Thread / Parker;Virtual Thread / Carrier
作用:竞争或等待时暂停、恢复相应执行单元
        │
        ▼
x86-64 Hardware
实例:Atomic Instruction / Cache Coherence / Memory Ordering
作用:提供 Atomicity、Visibility 与 Ordering 的硬件基础

1.2 一次 put / take 的完整实现路径

空队列上的 take 和随后 put 如何经过 JDK Queue 与 AQS、线程等待以及底层原子操作完成交接。

JDK 内部的核心流程:

同一把 ReentrantLock 保护环形队列;锁竞争和队列条件等待通过不同队列协调,恢复后重新检查条件。

1.3 Runtime / Language Implementation 层的三种保证

1.3.1 Atomicity

这里的 Atomicity 指一次入队或出队对队列状态的修改不会与其他线程的操作交错。

看 1.2 节的第二张图——「ArrayBlockingQueue put / take 内部路径」。put 和 take 都先调用 lockInterruptibly(),拿到锁后才执行 enqueue(e) 或 dequeue()。数组、索引和 count 的更新都在锁的保护下完成,其他线程无法在中途修改队列。

1.3.2 Visibility 和 Ordering

这里的 Visibility 指Consumer 能看到 Producer 入队前写入的数据;Ordering 指Producer 的这些写入先于 Consumer 的读取生效。

看 1.2 节的第一张图——「ArrayBlockingQueue 跨层实现路径」。Producer 先写入 message.delta = 1,再调用 jobs.put(message) 并释放锁(Release)。Consumer 随后获取同一把锁(Acquire),从 take() 取出消息后就能读到 delta == 1。因为前一次解锁 happens-before 随后的加锁,Producer 的写入对 Consumer 可见。notEmpty.signal() 则负责通知正在等待的 Consumer。

1.4 Hardware 层的三种保证

这些保证最终通过原子指令、Cache Coherence 和内存顺序约束落实:

Atomicity
  → Atomic Instruction
  → 锁状态竞争:LOCK CMPXCHG 等原子操作

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

Ordering
  → Acquire / Release 与必要的 Memory Ordering
  → x86-64 内存顺序约束 + LOCKed RMW / 编译器约束

相关实现:ArrayBlockingQueue.java、AbstractQueuedSynchronizer.java、ReentrantLock.java。


2. Go Runtime 如何实现 Channel?

2.1 分层

Go Source Code
实例:jobs <- message / message := <-jobs
作用:声明 Send / Receive 及同步关系
        │
        ▼
Go Implementation
实例:runtime/chan.go、chansend / chanrecv、hchan、sudog
作用:管理缓冲区、等待队列及消息配对
        │
        ▼
Go Runtime / Scheduler(G / M / P)
实例:gopark / goready
作用:暂停等待的 G,将被唤醒的 G 置为可运行状态
        │
        ▼
OS Thread / Thread Parking
实例:承载 G 的 M、内部锁竞争时的线程级等待
作用:运行 Goroutine;必要时暂停或唤醒 M
        │
        ▼
x86-64 Hardware
实例:Atomic Instruction / Cache Coherence / Memory Ordering
作用:提供 Atomicity、Visibility 与 Ordering 的硬件基础

2.2 一次 Channel 通信的完整实现路径

先看「Go Channel 跨层实现路径」时序图,以无缓冲 Channel 为例:

无缓冲 Channel 中,Consumer 先等待;Producer 到来后直接交接消息,并唤醒 Consumer。

Channel 的核心通信流程:

发送或接收先获取 Channel 内部锁。缓冲区可用时直接交接消息,否则登记等待并暂停 Goroutine。

2.3 Runtime / Language Implementation 层的三种保证

2.3.1 Atomicity

这里的 Atomicity 指一次 Channel 收发涉及的状态修改和消息配对,不会与其他 Goroutine 的操作交错。

看 2.2 节的第二张图——「Channel 的核心通信流程」。Runtime 先获取 hchan.lock:有缓冲时,修改 buf、sendx、recvx 和 qcount;无缓冲时,从 recvq 或 sendq 找到等待者并完成消息交接。如果暂时无法通信,Goroutine 会通过 gopark 等待并释放锁,等另一方完成交接后再由 goready 唤醒。

2.3.2 Visibility 和 Ordering

这里的 Visibility 指Consumer 完成接收后,能看到 Producer 发送前写入的数据;Ordering 指这些写入先于 Consumer 接收后的读取生效。

看 2.2 节的第一张图——「Go Channel 跨层实现路径」。Producer 先写入 message.delta = 1,再通过无缓冲 Channel 发送;sendDirect 将消息交给等待中的 Consumer,Consumer 完成接收后就能读到 delta == 1。Go 内存模型规定,一次发送 synchronized-before 对应接收操作完成。有缓冲 Channel 虽然先存储消息,也遵循这条规则。

2.4 Hardware 层的三种保证

Channel 的锁最终也要靠底层原子指令和内存顺序约束来实现:

Atomicity
  → Atomic Instruction
  → Runtime 内部锁的 CAS / Exchange 等原子操作

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

Ordering
  → Acquire / Release 与必要的 Memory Ordering
  → 编译器约束 + x86-64 内存顺序 / LOCKed RMW

源码:runtime/chan.go、runtime/proc.go、internal/runtime/atomic/atomic_amd64.s。


3. CPython 如何实现 queue.Queue?

3.1 分层

Python Source Code
实例:queue.Queue.put() / get()
作用:声明消息入队、出队及等待条件
        │
        ▼
CPython Standard Library
实例:queue.Queue / threading.Condition / collections.deque
作用:维护队列状态、检查条件并通知等待者
        │
        ▼
CPython Implementation
实例:_thread.lock / PyMutex / Parking Lot
作用:实现底层锁的原子状态与线程等待
        │
        ▼
OS Thread / Thread Parking
实例:Native Thread / Semaphore 等平台等待原语
作用:锁竞争或条件等待时阻塞与恢复线程
        │
        ▼
x86-64 Hardware
实例:Atomic Instruction / Cache Coherence / Memory Ordering
作用:提供 Atomicity、Visibility 与 Ordering 的硬件基础

3.2 一次 put / get 的完整实现路径

先看「CPython queue.Queue 跨层实现路径」时序图,以 Consumer 等待空队列为例:

Consumer 在空队列上等待;Producer 入队并通知后,Consumer 重新获得锁并取出消息。

再看「CPython Queue put / get 核心流程」:

put 和 get 在互斥锁保护下检查队列状态,满或空时等待通知,重新获取锁后再检查条件。

3.3 Runtime / Language Implementation 层的三种保证

3.3.1 Atomicity

这里的 Atomicity 指一次 put 或 get 中的队列检查与修改,不会与其他线程的队列操作交错。

看 3.2 节的第二张图——「CPython Queue put / get 核心流程」。put 和 get 都先获取 Queue.mutex,检查队列状态后,再执行 deque.append(item) 或 deque.popleft()。队列满或空时,Condition.wait() 会释放锁等待;收到通知后,线程重新获取锁并检查条件,才能继续操作。

3.3.2 Visibility 和 Ordering

这里的 Visibility 指Consumer 能看到 Producer 入队前写入的消息内容;Ordering 指这些写入先于 Consumer 的读取生效。

看 3.2 节的第一张图——「CPython queue.Queue 跨层实现路径」。Producer 先写入 message.delta = 1,再调用 put(message) 入队并释放 Queue.mutex(Release)。Consumer 收到通知、重新获取同一把锁(Acquire)后,从 get() 取出消息就能读到 delta == 1。这里由锁的释放和获取保证同步;not_empty.notify() 负责通知等待的线程。

3.4 Hardware 层的三种保证

再往下看,Queue.mutex 最终由 _thread.lock 实现。底层通过 PyMutex 的原子操作竞争锁;抢不到锁时,再借助 Parking Lot 和系统等待机制让线程暂停:

Atomicity
  → Atomic Instruction
  → PyMutex 锁状态的 CAS / Exchange

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

Ordering
  → Acquire / Release 与必要的 Memory Ordering
  → 编译器原子内存顺序 + CPU 顺序约束

源码:Lib/queue.py、Lib/threading.py、Python/lock.c、Python/parking_lot.c。


4. 三种实现的共同套路

以容量为 2 的 Queue 为例,两个 Producer 和两个 Consumer 同时操作,需要解决三个问题:

问题 如果不解决 对应机制
多个 Producer / Consumer 如何安全交接消息? 消息可能被覆盖,或被重复取出 Mutex / Atomicity
暂时无法交接消息时怎么办? 队列空了无法取、满了无法放,等待还可能白白占用 CPU Condition / Park / Wakeup
Consumer 为什么能读到 Producer 写入的数据? 消息虽然交接成功,读取到的内容却未必正确 Release / Acquire、Visibility / Ordering

4.1 谁能入队或出队?——Mutex / Atomicity

初始时,Queue 有两个空位,下一次写入位置为 tail = 0。如果两个 Producer 同时读取这个位置:

Producer A                    Producer B

读取 tail = 0                 读取 tail = 0
写入 buffer[0] = A            写入 buffer[0] = B

结果:两个 Producer 写入同一槽位,其中一条消息被覆盖

问题在于,读取写入位置与更新队列状态之间,其他 Producer 也可能插进来。

给 Queue 加上一把 Mutex,假设 Producer A 先获得锁:

Queue 初始:[空,空],tail = 0

Producer A                    Producer B

Lock 成功                     尝试 Lock → 等待
读取 tail = 0
写入槽位 0:A
更新 tail = 1
Unlock ──────────────────→     Lock 成功
                              重新读取 tail = 1
                              写入槽位 1:B
                              更新 tail = 0
                              Unlock

最终 Queue:[A,B]

Producer B 必须等 A 释放锁后,才能读取更新后的 tail,因此不会再写入槽位 0。谁先获得锁并不确定,但两次成功入队不会相互覆盖。

两个 Consumer 并发出队也是同一个道理:检查队列状态、取出消息和更新状态必须在同一临界区内完成,才能避免重复取出。Mutex 底层通过 Atomic 操作竞争锁状态,从而保证 Queue 操作的 Atomicity。

4.2 Queue 为空或已满怎么办?——Condition / Park / Wakeup

互斥保证了交接时不发生冲突,却不能解决队列为空或已满时的等待。

Queue 为空 → Consumer 等待 notEmpty
Queue 已满 → Producer 等待 notFull

以 Consumer 先遇到空队列为例:

Consumer                         Producer

Lock → 发现 Queue 为空
登记 notEmpty 等待
Unlock → Park
                                 Lock → 入队
                                 signal(notEmpty)
                                 Unlock
Wakeup → Lock
重新检查 Queue 状态
取出消息 → Unlock

Producer 遇到满队列时则反过来:等待 notFull,由 Consumer 出队后通知。

等待必须释放锁,否则另一方无法改变 Queue 的状态;唤醒后必须重新检查条件,因为其他线程可能先一步完成交接。登记等待与释放锁还必须正确协调,避免丢失通知。

4.3 交接完成后,为什么能读到消息内容?——Release / Acquire

前两个问题保证了交接不会冲突,也不会因为空满而一直占住锁。但 Consumer 还需要看到 Producer 发送前写入的数据。

Producer                             Consumer

写入 message.delta = 1
         ↓
发布消息 [Release] ─── 同步交接 ───▶ 接收消息 [Acquire]
                                           ↓
                                     读取 delta == 1

对于使用 Mutex 的 Queue,Producer 释放锁(Release)happens-before Consumer 随后获取同一把锁(Acquire)。因此,Consumer 能看到 Producer 在发送消息前的写入。

Go 的无缓冲 Channel 则不需要消息缓冲区,而是直接配对发送者与接收者,并保证发送前的写入对接收方可见。

加上保存消息的缓冲区和队列状态,这三个机制就构成了一个有界阻塞 Queue 的核心:

消息缓冲区 + 队列状态
       +
Mutex:保证一次交接不可被打断
       +
Condition:空满时等待与唤醒
       +
Release / Acquire:保证消息可见、读写有序
       ↓
Blocking Queue

5. 下一篇:两种协作模型的关系

下一篇将讨论 Shared Memory 与 Message Passing 的关系,以及它们在实际程序中如何组合使用。第十二篇则作为终章,回顾和总结整个并发编程系列。


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

posted @ 2026-10-15 19:52  ThinkerQAQ  阅读(31)  评论(0)    收藏  举报