Files
test/chapters/chapter-5-concurrency-deep.md

409 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第五章:并发编程 —— 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 ChannelGoroutine 间的通信(深度解析)
> **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深度图解版*