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

8.0 KiB
Raw Blame History

第五章:并发编程 —— Goroutine 与 Channel 的艺术(图解版)

本章目标:深入理解 Go 并发的核心机制,配合大量图解,掌握 Goroutine 调度原理、Channel 通信模式、同步原语等。

5.1 并发基础与 Goroutine

5.1.1 并发 vs 并行(图解)

graph LR
    subgraph 并发 Concurrency
        A[任务 1] -->|交替执行 | B(时间片 1)
        A -->|交替执行 | C(时间片 3)
        D[任务 2] -->|交替执行 | B
        D -->|交替执行 | C
        style A fill:#f9f,stroke:#333
        style D fill:#f9f,stroke:#333
    end

    subgraph 并行 Parallelism
        E[任务 1] -->|同时执行 | F(CPU 核心 1)
        G[任务 2] -->|同时执行 | H(CPU 核心 2)
        style E fill:#9f9,stroke:#333
        style G fill:#9f9,stroke:#333
    end
  • 并发:宏观上同时,微观上交替(单核也能实现)。
  • 并行:微观上同时(需要多核 CPU

5.2 GMP 调度模型 核心重点(图解)

Go 的并发性能得益于其独特的 GMP 调度模型

5.2.1 GMP 模型架构(图解)

graph TB
    subgraph "Go Runtime 用户态调度"
        P1[P1: 逻辑处理器]
        P2[P2: 逻辑处理器]
        P3[P3: 逻辑处理器]
        
        Q1[本地 G 队列]
        Q2[本地 G 队列]
        Q3[本地 G 队列]
        
        P1 --- Q1
        P2 --- Q2
        P3 --- Q3
        
        GlobalQ[全局 G 队列]
        GlobalQ -.-> P1
        GlobalQ -.-> P2
        GlobalQ -.-> P3
    end
    
    subgraph "操作系统内核态"
        M1[M1: 线程]
        M2[M2: 线程]
        M3[M3: 线程]
        
        M1 <--> P1
        M2 <--> P2
        M3 <--> P3
    end
    
    subgraph "Goroutine 实体"
        G1[G1: 协程]
        G2[G2: 协程]
        G3[G3: 协程]
        G4[G4: 协程]
        
        G1 --> Q1
        G2 --> Q1
        G3 --> Q2
        G4 --> Q3
    end
    
    style P1 fill:#ffeb3b,stroke:#333
    style M1 fill:#2196f3,stroke:#333,color:#fff
    style G1 fill:#4caf50,stroke:#333,color:#fff

图解说明

  1. G (Goroutine):绿色的协程,包含栈、指令指针。
  2. P (Processor):黄色的逻辑处理器,管理 G 队列,数量 = CPU 核数
  3. M (Machine):蓝色的操作系统线程,真正执行代码。
  4. 调度M 绑定 P从 P 的本地队列取 G 执行。

5.2.2 调度流程:从创建到执行

sequenceDiagram
    participant App as 应用程序
    participant G as Goroutine
    participant P as P (逻辑处理器)
    participant M as M (线程)
    participant OS as 操作系统

    App->>P: 创建 G放入本地队列
    P->>M: M 绑定 P获取 G
    M->>G: 执行 G 代码
    alt G 阻塞 (如 IO)
        G->>OS: 发起系统调用
        OS-->>M: M 阻塞
        P->>P: 寻找新 M 继续调度其他 G
        Note right of P: P 不阻塞,继续工作!
    else G 完成
        G->>M: 执行完毕
        M->>P: 归还 P
    end

5.2.3 工作窃取 (Work Stealing)

当某个 P 的队列为空时,它会从其他 P 的队列窃取一半的 G。

P1 队列:[G1, G2, G3, G4, G5]  (满载)
P2 队列:[]                     (空闲)

P2 发现空 -> 向 P1 请求 -> P1 给 G4, G5
P1 队列:[G1, G2, G3]
P2 队列:[G4, G5]  <-- 窃取成功!

5.3 ChannelGoroutine 间的通信(图解)

5.3.1 无缓冲 Channel (同步)

发送和接收必须同时就绪,否则阻塞。

发送方 (go func)          接收方 (main)
      |                        |
      |  ch <- 42              |
      |  (阻塞等待)            |
      | <------------------->  |  数据传递 (42)
      |                        |  (同时发生)
      v                        v
   发送完成                 接收完成

5.3.2 有缓冲 Channel (异步)

缓冲区未满时,发送不阻塞;缓冲区非空时,接收不阻塞。

缓冲区容量 = 2

发送方                缓冲区                接收方
  |                    [ ] [ ]                 |
  | ch <- 1 (成功)     [1] [ ]                 |
  | ch <- 2 (成功)     [1] [2]                 |
  | ch <- 3 (阻塞!)    [1] [2]                 |  (满了)
  |                    [1] [2]                 | <-val (接收 1)
  |                    [ ] [2]                 |
  | ch <- 3 (成功!)    [3] [2]                 |
  |                    [3] [2]                 | <-val (接收 2)
  |                    [3] [ ]                 |

5.3.3 Channel 状态机

stateDiagram-v2
    [*] --> 未初始化
    未初始化 --> 打开make
    打开 --> 打开:发送/接收
    打开 --> 关闭close()
    关闭 --> 关闭:接收 (返回零值)
    关闭 --> [*]GC 回收
    打开 --> [*]GC 回收
    
    note right of 打开
      发送阻塞:缓冲区满
      接收阻塞:缓冲区空
    end note

5.4 同步原语 (图解)

5.4.1 WaitGroup 计数原理

初始Counter = 0

Goroutine 1: Add(1) -> Counter = 1
Goroutine 2: Add(1) -> Counter = 2
Goroutine 3: Add(1) -> Counter = 3

Goroutine 1: Done() -> Counter = 2
Goroutine 2: Done() -> Counter = 1
Goroutine 3: Done() -> Counter = 0 (唤醒 Wait())

5.4.2 Mutex 锁状态

stateDiagram-v2
    [*] --> 空闲:初始
    空闲 --> 占用Lock()
    占用 --> 空闲Unlock()
    占用 --> 等待队列Lock() (阻塞)
    等待队列 --> 空闲Unlock() (唤醒)

5.4.3 RWMutex 读写锁

读锁 (RLock):允许多个读者同时持有
写锁 (Lock):排他,只能有一个写者,且不能有读者

状态图:
[空闲] --R--> [多读] --R--> [多读]
  |              |
  L              L
  v              v
[写] <--------- [多读] (写者等待)
  |
  U
  v
[空闲]

5.5 原子操作 (图解)

内存地址0x1000 (值 = 5)

Goroutine 1: atomic.Add(0x1000, 1) -> 硬件级 CAS -> 值 = 6
Goroutine 2: atomic.Add(0x1000, 1) -> 硬件级 CAS -> 值 = 7
(无需锁CPU 指令直接保证原子性)

5.6 Context 传递 (图解)

graph LR
    Parent[父 Context] -->|WithTimeout| Child1[子 Context 1]
    Parent -->|WithValue| Child2[子 Context 2]
    
    Parent -- 取消 --> Cancel1[取消信号]
    Child1 -- 传播 --> Cancel1
    Child2 -- 传播 --> Cancel1
    
    style Parent fill:#ff9800,stroke:#333
    style Cancel1 fill:#f44336,stroke:#333,color:#fff
  • 取消传播:父 Context 取消,所有子 Context 自动取消。
  • 超时传播:父 Context 超时,子 Context 也超时。

5.7 竞态检测 (图解)

竞态 (Race Condition)
Goroutine 1: 读 变量 X
Goroutine 2: 写 变量 X
(无锁,同时发生) -> 数据不一致!

Race Detector 检测:
Goroutine 1: 读 X (记录时间 T1)
Goroutine 2: 写 X (记录时间 T2)
T1 和 T2 重叠 -> 报告竞态!

5.8 并发设计模式 (图解)

5.8.1 管道 (Pipeline)

Stage 1 (生成)      Stage 2 (平方)      Stage 3 (输出)
   [1,2,3]  ----->   [1,4,9]  ----->   打印
      |                 |                 |
   (Chan A)          (Chan B)           (结果)

5.8.2 工作池 (Worker Pool)

任务队列 (100 个)
      |
      v
+-------------------+
| Worker 1 (处理)   |
| Worker 2 (处理)   |  (并发执行)
| Worker 3 (处理)   |
+-------------------+
      |
      v
结果队列

(本章其余部分保持原有文字内容,此处仅展示图解核心)


🎨 图解总结

  1. GMP 模型P 是调度器M 是执行者G 是任务。
  2. Channel:无缓冲是同步,有缓冲是异步。
  3. Mutex 互斥RWMutex 读写分离。
  4. Context:树状结构,取消信号自顶向下传播。
  5. 管道:数据流式处理,解耦各阶段。

代码仓库位置https://giter.top/openclaw/test/tree/main/chapters/chapter-5

下一章预告HTTP 服务器、路由、中间件、数据库连接池、RESTful API 设计、部署