Update Chapter 5 Deep Illustrated Version with Fixed Mermaid Syntax and Detailed Explanations
This commit is contained in:
408
chapters/chapter-5-concurrency-deep.md
Normal file
408
chapters/chapter-5-concurrency-deep.md
Normal file
@@ -0,0 +1,408 @@
|
||||
# 第五章:并发编程 —— Goroutine 与 Channel 的艺术(深度图解版)
|
||||
|
||||
> **本章目标**:结合经典源码分析文章,通过**"图 + 深度解析"**的方式,彻底搞懂 Go 并发的底层原理。
|
||||
> **参考文章**:
|
||||
> 1. Go 并发:goroutine、channel 与 select 实战指南
|
||||
> 2. Go 语言 GMP 模型深度解析
|
||||
> 3. Go 语言 Channel 源码详解
|
||||
> 4. Go 语言 Mutex 源码解析与优化
|
||||
> 5. Go 语言 Context 源码解析
|
||||
|
||||
---
|
||||
|
||||
## 5.1 并发基础与 Goroutine
|
||||
|
||||
### 5.1.1 并发 vs 并行
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph 并发 Concurrency
|
||||
A[任务 1] -->|时间片轮转 | B(核心 1)
|
||||
D[任务 2] -->|时间片轮转 | B
|
||||
style A fill:#f9f,stroke:#333
|
||||
style D fill:#f9f,stroke:#333
|
||||
style B fill:#ffeb3b,stroke:#333
|
||||
end
|
||||
subgraph 并行 Parallelism
|
||||
E[任务 1] -->|同时 | F(核心 1)
|
||||
G[任务 2] -->|同时 | H(核心 2)
|
||||
style E fill:#9f9,stroke:#333
|
||||
style G fill:#9f9,stroke:#333
|
||||
end
|
||||
```
|
||||
|
||||
**📖 图解深度解析**:
|
||||
- **并发 (Concurrency)**:
|
||||
- **核心特征**:宏观上同时,微观上交替。
|
||||
- **实现方式**:单核 CPU 通过**时间片轮转**快速切换任务,给人同时的错觉。
|
||||
- **Go 的优势**:Go 的调度器在**用户态**实现,切换开销极小(微秒级),远优于操作系统线程切换(毫秒级)。
|
||||
- **并行 (Parallelism)**:
|
||||
- **核心特征**:微观上真正的同时。
|
||||
- **实现方式**:需要**多核 CPU**,每个任务占用一个核心同时执行。
|
||||
- **Go 配置**:通过 `runtime.GOMAXPROCS(n)` 设置最大 P 数量(通常等于 CPU 核数),开启并行。
|
||||
|
||||
---
|
||||
|
||||
## 5.2 GMP 调度模型 核心重点(深度解析)
|
||||
|
||||
Go 的并发性能核心在于 **GMP 模型**。它解决了操作系统线程调度开销大、无法充分利用多核的问题。
|
||||
|
||||
### 5.2.1 GMP 模型架构
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Go Runtime 用户态调度
|
||||
P1[P1: 逻辑处理器]
|
||||
P2[P2: 逻辑处理器]
|
||||
Q1[本地 G 队列 Local]
|
||||
Q2[本地 G 队列 Local]
|
||||
P1 --- Q1
|
||||
P2 --- Q2
|
||||
GlobalQ[全局 G 队列 Global]
|
||||
GlobalQ -.->|偶尔调度 | P1
|
||||
GlobalQ -.->|偶尔调度 | P2
|
||||
end
|
||||
subgraph 操作系统内核 Kernel
|
||||
M1[M1: 线程]
|
||||
M2[M2: 线程]
|
||||
M1 <-->|绑定 | P1
|
||||
M2 <-->|绑定 | P2
|
||||
end
|
||||
subgraph Goroutine 实体
|
||||
G1[G1: 协程]
|
||||
G2[G2: 协程]
|
||||
G3[G3: 协程]
|
||||
G1 --> Q1
|
||||
G2 --> Q1
|
||||
G3 --> GlobalQ
|
||||
end
|
||||
style P1 fill:#ffeb3b,stroke:#333,stroke-width:2px
|
||||
style M1 fill:#2196f3,stroke:#333,stroke-width:2px,color:#fff
|
||||
style G1 fill:#4caf50,stroke:#333,stroke-width:2px,color:#fff
|
||||
style GlobalQ fill:#ff9800,stroke:#333
|
||||
```
|
||||
|
||||
**📖 图解深度解析**:
|
||||
1. **G (Goroutine)**:
|
||||
- **结构**:`g` 结构体,包含栈(初始 2KB,动态增长)、指令指针、状态(idle, running, syscall 等)。
|
||||
- **轻量**:栈内存由 Go 运行时管理,创建开销极小(约 2KB),可轻松创建百万级。
|
||||
- **图中位置**:绿色节点,被放入 P 的本地队列或全局队列。
|
||||
2. **P (Processor)**:
|
||||
- **作用**:**调度器**的核心,管理 G 队列,提供执行环境(如内存分配器缓存)。
|
||||
- **数量**:等于 `GOMAXPROCS`,通常等于 CPU 核数。P 的数量限制了**最大并行度**。
|
||||
- **本地队列**:每个 P 维护一个本地队列,最多容纳 256 个 G。优先从本地队列取 G,减少锁竞争。
|
||||
- **图中位置**:黄色节点,是 G 和 M 之间的**桥梁**。
|
||||
3. **M (Machine)**:
|
||||
- **作用**:操作系统线程,**真正执行代码**的实体。
|
||||
- **绑定**:M 必须绑定一个 P 才能执行 G。M 和 P 是**动态绑定**的。
|
||||
- **图中位置**:蓝色节点,代表操作系统线程。
|
||||
4. **全局队列 (Global Queue)**:
|
||||
- **作用**:存放新创建的 G,当 P 的本地队列满时,或启动时,G 会进入全局队列。
|
||||
- **调度频率**:P 优先从本地队列取 G,只有本地队列为空时,才会尝试从全局队列或**窃取**其他 P 的 G。
|
||||
- **图中位置**:橙色节点,作为备用队列。
|
||||
|
||||
### 5.2.2 调度流程:从创建到执行
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App as 应用程序
|
||||
participant P as P 逻辑处理器
|
||||
participant M as M 线程
|
||||
participant OS as 操作系统
|
||||
|
||||
App->>P: 创建 G,放入本地队列
|
||||
Note over P: 本地队列未满
|
||||
P->>M: M 已绑定 P,检查本地队列
|
||||
M->>G: 取出 G 并执行
|
||||
alt G 阻塞 如 IO
|
||||
G->>OS: 发起系统调用
|
||||
OS-->>M: M 阻塞
|
||||
Note over P: P 脱离 M,继续调度其他 G
|
||||
P->>P: 寻找新 M 或创建新 M
|
||||
P->>NewM: 绑定新 M,继续执行
|
||||
else G 完成
|
||||
G->>M: 执行完毕
|
||||
M->>P: 归还 P
|
||||
end
|
||||
```
|
||||
|
||||
**📖 图解深度解析**:
|
||||
- **正常流程**:G 创建 -> 放入 P 的本地队列 -> M 从本地队列取 G -> 执行。
|
||||
- **阻塞处理(关键优化)**:
|
||||
- 当 G 阻塞(如网络 IO、文件 IO)时,**M 也会阻塞**(操作系统线程阻塞)。
|
||||
- 如果 M 一直阻塞,P 就无法调度其他 G,导致并行度下降。
|
||||
- **Go 的解决方案**:M 阻塞前,P 会**脱离** M,并寻找(或创建)一个新的 M 来继续执行 P 中的其他 G。
|
||||
- **结果**:P 始终有 M 执行,不会因为某个 G 阻塞而停滞。
|
||||
- **工作窃取 (Work Stealing)**:
|
||||
- 当 P 的本地队列为空,且全局队列为空时,P 会尝试从**其他 P 的本地队列**窃取一半的 G。
|
||||
- **目的**:负载均衡,避免某些 P 空闲而其他 P 队列堆积。
|
||||
|
||||
### 5.2.3 工作窃取细节
|
||||
|
||||
```
|
||||
P1 队列:G1, G2, G3, G4, G5, G6, G7, G8 (8 个 G)
|
||||
P2 队列:空 (空)
|
||||
|
||||
P2 发现空 -> 向 P1 请求窃取
|
||||
P1 响应:保留前 4 个,后 4 个给 P2
|
||||
P1 队列:G1, G2, G3, G4
|
||||
P2 队列:G5, G6, G7, G8 <-- 窃取成功!
|
||||
```
|
||||
|
||||
**📖 深度解析**:
|
||||
- **窃取策略**:从**尾部**窃取一半,因为尾部是最近加入的,可能更热。
|
||||
- **锁竞争**:窃取时需要加锁,但频率较低,性能开销可控。
|
||||
- **意义**:确保所有 CPU 核心都被充分利用,避免负载不均。
|
||||
|
||||
---
|
||||
|
||||
## 5.3 Channel:Goroutine 间的通信(深度解析)
|
||||
|
||||
> **Go 哲学**:不要通过共享内存来通信,而要通过通信来共享内存。
|
||||
|
||||
### 5.3.1 Channel 的底层结构 (hchan)
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph hchan 结构体
|
||||
qcount[qcount: 数据量]
|
||||
dataqsiz[dataqsiz: 缓冲区大小]
|
||||
buf[buf: 环形缓冲区指针]
|
||||
elemsize[elemsize: 元素大小]
|
||||
closed[closed: 是否关闭]
|
||||
sendx[sendx: 发送索引]
|
||||
recvx[recvx: 接收索引]
|
||||
recvq[recvq: 接收等待队列]
|
||||
sendq[sendq: 发送等待队列]
|
||||
lock[lock: 互斥锁]
|
||||
|
||||
qcount --> dataqsiz
|
||||
dataqsiz --> buf
|
||||
sendx --> sendq
|
||||
recvx --> recvq
|
||||
lock -.-> qcount
|
||||
lock -.-> sendx
|
||||
lock -.-> recvx
|
||||
end
|
||||
style hchan fill:#ffeb3b,stroke:#333
|
||||
style lock fill:#f44336,stroke:#333,color:#fff
|
||||
```
|
||||
|
||||
**📖 图解深度解析**:
|
||||
- **hchan**:Channel 的底层结构体,所有操作都围绕它进行。
|
||||
- **buf**:指向一个**环形缓冲区**(数组),存储实际数据。
|
||||
- **无缓冲**:`dataqsiz = 0`, `buf = nil`。
|
||||
- **有缓冲**:`dataqsiz > 0`,数据存入数组。
|
||||
- **sendx / recvx**:发送和接收的**索引**,环形移动。
|
||||
- **sendq / recvq**:等待队列(sudog 链表),存放阻塞的 Goroutine。
|
||||
- **发送阻塞**:缓冲区满 -> 加入 `sendq`。
|
||||
- **接收阻塞**:缓冲区空 -> 加入 `recvq`。
|
||||
- **lock**:**互斥锁**,保证并发安全。所有操作(发送、接收、关闭)都必须加锁。
|
||||
- **closed**:标记 Channel 是否关闭。
|
||||
|
||||
### 5.3.2 无缓冲 Channel (同步通信)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Sender as 发送方 G1
|
||||
participant Ch as Channel hchan
|
||||
participant Receiver as 接收方 G2
|
||||
|
||||
Sender->>Ch: 尝试发送 ch <- data
|
||||
Note over Ch: 缓冲区空 dataqsiz=0
|
||||
Ch->>Sender: 检查是否有接收者 recvq 非空
|
||||
alt 有接收者 G2 在 recvq
|
||||
Ch->>Receiver: 唤醒 G2
|
||||
Ch->>Sender: 直接拷贝数据 G1 -> G2
|
||||
Ch->>Sender: 唤醒 G1
|
||||
Note over Ch: 无需经过缓冲区!
|
||||
else 无接收者
|
||||
Ch->>Sender: 将 G1 加入 sendq
|
||||
Ch->>Sender: 阻塞 G1
|
||||
end
|
||||
```
|
||||
|
||||
**📖 图解深度解析**:
|
||||
- **同步机制**:发送和接收必须**同时就绪**。
|
||||
- **直接拷贝**:如果接收方已经在等待(在 `recvq` 中),数据**直接从发送方拷贝到接收方**,**不经过缓冲区**。
|
||||
- **阻塞逻辑**:
|
||||
- 发送方:如果没有接收者,加入 `sendq` 并阻塞。
|
||||
- 接收方:如果没有发送者,加入 `recvq` 并阻塞。
|
||||
- **性能**:无缓冲 Channel 的开销主要是**上下文切换**(唤醒对方)。
|
||||
|
||||
### 5.3.3 有缓冲 Channel (异步通信)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Sender as 发送方
|
||||
participant Ch as Channel 缓冲区=2
|
||||
participant Receiver as 接收方
|
||||
|
||||
Sender->>Ch: ch <- 1
|
||||
Note over Ch: 缓冲区未满 -> 写入 buf0
|
||||
Sender->>Ch: ch <- 2
|
||||
Note over Ch: 缓冲区未满 -> 写入 buf1
|
||||
Sender->>Ch: ch <- 3
|
||||
Note over Ch: 缓冲区已满 -> 加入 sendq -> 阻塞
|
||||
|
||||
Receiver->>Ch: <-ch
|
||||
Note over Ch: 从 buf0 读取 -> recvx++
|
||||
Receiver->>Ch: <-ch
|
||||
Note over Ch: 从 buf1 读取 -> recvx++
|
||||
Receiver->>Ch: <-ch
|
||||
Note over Ch: 缓冲区空 -> 检查 sendq
|
||||
Ch->>Sender: 唤醒 sendq 中的 G -> 直接拷贝
|
||||
```
|
||||
|
||||
**📖 图解深度解析**:
|
||||
- **缓冲区**:数据先存入 `buf` 数组,发送方不阻塞(直到满)。
|
||||
- **环形移动**:`sendx` 和 `recvx` 循环移动,利用固定大小数组。
|
||||
- **阻塞逻辑**:
|
||||
- 发送方:缓冲区满 -> 加入 `sendq` 阻塞。
|
||||
- 接收方:缓冲区空 -> 检查 `sendq`,如果有等待的发送者,直接拷贝;否则加入 `recvq` 阻塞。
|
||||
- **性能**:有缓冲 Channel 减少了上下文切换,但增加了内存开销。
|
||||
|
||||
### 5.3.4 关闭 Channel 的底层逻辑
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> 打开
|
||||
打开 --> 关闭:closech
|
||||
关闭 --> 关闭:接收 返回零值
|
||||
关闭 --> [*]:GC
|
||||
|
||||
note right of 关闭
|
||||
1. 设置 closed=1
|
||||
2. 唤醒所有 recvq 返回零值
|
||||
3. 唤醒所有 sendq panic
|
||||
end note
|
||||
```
|
||||
|
||||
**📖 图解深度解析**:
|
||||
- **设置标志**:`closed = 1`。
|
||||
- **唤醒接收者**:遍历 `recvq`,唤醒所有等待的 G,它们读取时会得到**零值**。
|
||||
- **唤醒发送者**:遍历 `sendq`,唤醒所有等待的 G,它们会 **panic**(向已关闭的 Channel 发送是错误)。
|
||||
- **幂等性**:重复关闭会 panic。
|
||||
|
||||
---
|
||||
|
||||
## 5.4 同步原语 (深度解析)
|
||||
|
||||
### 5.4.1 Mutex 的优化:自旋与饥饿
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> 空闲:初始
|
||||
空闲 --> 占用:Lock
|
||||
占用 --> 自旋:Lock 短暂等待
|
||||
自旋 --> 占用:获取锁
|
||||
自旋 --> 饥饿:等待过久
|
||||
饥饿 --> 占用:被唤醒
|
||||
占用 --> 空闲:Unlock
|
||||
|
||||
note right of 自旋
|
||||
CPU 循环检查锁状态<br/>避免上下文切换开销
|
||||
end note
|
||||
note right of 饥饿
|
||||
防止饿死,优先让等待者<br/>获取锁
|
||||
end note
|
||||
```
|
||||
|
||||
**📖 图解深度解析**:
|
||||
- **自旋 (Spinning)**:
|
||||
- 当锁被占用时,Go 不会立即阻塞,而是先**循环检查**(自旋)几十次。
|
||||
- **原因**:如果锁很快释放,自旋比上下文切换(阻塞+唤醒)更快。
|
||||
- **适用场景**:锁持有时间极短。
|
||||
- **饥饿模式 (Starvation)**:
|
||||
- 如果自旋后仍未获取锁,进入阻塞队列。
|
||||
- 如果等待时间过长(>1ms),进入**饥饿模式**。
|
||||
- **饥饿模式逻辑**:新来的 G 不抢锁,直接让给等待最久的 G,防止饿死。
|
||||
- **性能优化**:自旋 + 饥饿模式,平衡了性能和公平性。
|
||||
|
||||
### 5.4.2 RWMutex 的读写分离
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph RWMutex 状态
|
||||
ReaderCount[readerCount: 读者数]
|
||||
ReaderWait[readerWait: 等待读者]
|
||||
Mutex[Mutex: 底层互斥锁]
|
||||
|
||||
ReaderCount --> Mutex
|
||||
ReaderWait --> Mutex
|
||||
end
|
||||
|
||||
subgraph 读锁 RLock
|
||||
R1[读者 1] -->|readerCount++ | Mutex
|
||||
R2[读者 2] -->|readerCount++ | Mutex
|
||||
R1 -.->|同时持有 | R2
|
||||
end
|
||||
|
||||
subgraph 写锁 Lock
|
||||
W1[写者] -->|readerCount=0 | Mutex
|
||||
W1 -.->|独占 | W1
|
||||
end
|
||||
|
||||
style Mutex fill:#f44336,stroke:#333,color:#fff
|
||||
style R1 fill:#4caf50,stroke:#333,color:#fff
|
||||
style W1 fill:#ff9800,stroke:#333,color:#fff
|
||||
```
|
||||
|
||||
**📖 图解深度解析**:
|
||||
- **readerCount**:记录当前读者数量。
|
||||
- **readerWait**:记录等待的写者数量(防止写者饿死)。
|
||||
- **读锁**:
|
||||
- 如果 `readerCount > 0` 或 `readerWait > 0`,可能阻塞。
|
||||
- 多个读者可以**同时持有**读锁。
|
||||
- **写锁**:
|
||||
- 必须 `readerCount == 0` 且 `readerWait == 0` 才能获取。
|
||||
- 写锁是**独占**的。
|
||||
- **写者优先**:当有写者等待时,新来的读者会阻塞,防止写者饿死。
|
||||
|
||||
---
|
||||
|
||||
## 5.5 Context 的树状传播 (深度解析)
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Parent[父 Context] -->|WithCancel| Child1[子 Context 1]
|
||||
Parent -->|WithTimeout| Child2[子 Context 2]
|
||||
Child1 -->|WithValue| Grandchild[孙子 Context]
|
||||
|
||||
Parent -- 取消信号 --> Cancel1[取消通道]
|
||||
Child1 -- 传播 --> Cancel1
|
||||
Child2 -- 传播 --> Cancel1
|
||||
Grandchild -- 传播 --> Cancel1
|
||||
|
||||
style Parent fill:#ff9800,stroke:#333
|
||||
style Cancel1 fill:#f44336,stroke:#333,color:#fff
|
||||
```
|
||||
|
||||
**📖 图解深度解析**:
|
||||
- **树状结构**:Context 通过 `parent` 字段形成树状结构。
|
||||
- **取消传播**:
|
||||
- 父 Context 取消 -> 关闭父的 `done` 通道。
|
||||
- 子 Context 监听父的 `done` -> 自动关闭自己的 `done`。
|
||||
- **级联效应**:整棵树的所有 Context 都会收到取消信号。
|
||||
- **WithValue**:
|
||||
- 子 Context 继承父 Context 的键值对。
|
||||
- **注意**:不要频繁使用 `WithValue`,避免传递过多数据。
|
||||
|
||||
---
|
||||
|
||||
## 5.6 总结:并发设计的核心思想
|
||||
|
||||
1. **GMP 模型**:用户态调度,轻量级,充分利用多核。
|
||||
2. **Channel 通信**:通过通信共享内存,避免锁竞争。
|
||||
3. **锁优化**:自旋 + 饥饿模式,平衡性能与公平。
|
||||
4. **Context 传播**:树状结构,统一取消控制。
|
||||
|
||||
---
|
||||
|
||||
**代码仓库位置**:https://giter.top/openclaw/test/tree/main/chapters/chapter-5
|
||||
|
||||
**深度图解文档**:https://giter.top/openclaw/test/blob/main/chapters/chapter-5-concurrency-deep.md
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2026-03-23 23:55 UTC(深度图解版)*
|
||||
Reference in New Issue
Block a user