diff --git a/chapters/chapter-5-revised.md b/chapters/chapter-5-revised.md new file mode 100644 index 0000000..0e5b127 --- /dev/null +++ b/chapters/chapter-5-revised.md @@ -0,0 +1,365 @@ +# 第五章:并发编程 —— 让程序“多任务”并行 + +> **👋 回顾一下:** +> +> 前四章,我们学会了基础语法、数据结构、函数和接口。现在,你已经能写出逻辑清晰、结构良好的代码了。 +> +> **🤔 但是,现实世界的任务往往很复杂……** +> +> 想象一下,你是一家餐厅的老板。 +> * 如果只有一个厨师(单线程),客人点菜 -> 厨师炒菜 -> 上菜。客人要等很久。 +> * 如果有十个厨师(多线程/多协程),可以**同时**处理十张订单,效率翻倍! +> +> 这就是**并发 (Concurrency)** 的魅力:**宏观上同时处理多个任务**(微观上可能是交替执行,也可能是真正的并行)。 +> +> Go 语言天生就是为并发而生的!它提供了**Goroutine**(轻量级线程)和**Channel**(通信管道),让并发编程变得像写串行代码一样简单。 +> +> **🎯 这一章,我们要掌握 Go 并发的核心:** +> 1. **Goroutine**:如何启动“小助手”? +> 2. **GMP 模型**:Go 是怎么调度的?(核心难点,有图解!) +> 3. **Channel**:Goroutine 之间怎么“传纸条”? +> 4. **同步原语**:Mutex、WaitGroup、Once(怎么避免“打架”?)。 +> 5. **Context**:怎么统一“叫停”所有任务? +> +> **别担心,我会用大量的图解和生活类比,带你轻松攻克这个“高难度”章节!** + +--- + +## 5.1 Goroutine —— 启动你的“小助手” + +> **💡 想象一下:** +> 你有一个大任务(比如处理 1000 个订单)。如果一个个处理,太慢了。 +> 于是,你雇佣了 1000 个**小助手**(Goroutine),每人处理一个订单。 +> +> 在 Go 中,启动一个小助手只需要一个关键字:**`go`**。 + +### 📝 启动 Goroutine + +```go +func sayHello(name string) { + fmt.Println("你好,", name) +} + +func main() { + go sayHello("Alice") // 启动一个小助手 + go sayHello("Bob") + go sayHello("Charlie") + + // 主函数如果退出,所有小助手都会消失! + // 所以需要等待(后面会讲 WaitGroup) + time.Sleep(1 * time.Second) +} +``` + +> **💡 老师的小揭秘:** +> * **轻量级**:一个 Goroutine 只占 2KB 内存(操作系统线程要 1-8MB)。 +> * **数量巨大**:可以轻松创建**百万级** Goroutine。 +> * **非阻塞**:`go` 启动后,主程序**立刻继续执行**,不会等待小助手完成。 + +> **⚠️ 老师的小提醒:** +> * 主函数 `main` 退出,所有 Goroutine 都会**强制终止**。 +> * 所以,如果需要等待 Goroutine 完成,必须用**同步机制**(如 `time.Sleep`、`WaitGroup`、`Channel`)。 + +> **🎮 动手练一练:** +> 写一个程序,启动 5 个 Goroutine,每个打印“我是第 X 个助手”。观察输出顺序(是不是每次都不一样?)。 + +--- + +## 5.2 GMP 模型 —— Go 的“调度魔法” ⭐ 核心重点 + +> **🤔 为什么 Go 这么快?** +> 因为 Go 的调度器(Scheduler)非常聪明!它不像操作系统那样直接调度线程,而是自己管理**Goroutine**,把任务分配给**操作系统线程**。 +> +> 这就是著名的 **GMP 模型**。 + +### 📖 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)**:绿色节点。 +> * **结构**:包含栈(2KB 起步)、指令指针、状态。 +> * **轻量**:创建开销极小,可创建百万级。 +> * **位置**:被放入 P 的本地队列或全局队列。 +> 2. **P (Processor)**:黄色节点。 +> * **作用**:**调度器**的核心,管理 G 队列,提供执行环境(如内存分配器)。 +> * **数量**:等于 `GOMAXPROCS`(通常=CPU 核数)。**P 的数量限制了最大并行度**。 +> * **本地队列**:每个 P 维护一个本地队列(最多 256 个 G),优先调度,减少锁竞争。 +> 3. **M (Machine)**:蓝色节点。 +> * **作用**:操作系统线程,**真正执行代码**的实体。 +> * **绑定**:M 必须绑定一个 P 才能执行 G。M 和 P 是**动态绑定**的。 +> 4. **全局队列 (Global Queue)**:橙色节点。 +> * **作用**:存放新创建的 G,当 P 的本地队列满时,或启动时,G 会进入全局队列。 +> * **调度频率**:P 优先从本地队列取 G,只有本地队列为空时,才会尝试从全局队列或**窃取**其他 P 的 G。 + +### 🔄 调度流程:从创建到执行 + +```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 阻塞(如 IO),M 也会阻塞。Go 的优化:P 会**脱离** M,并寻找/创建新 M 继续执行其他 G。 +> * **结果**:P 始终有 M 执行,不会因为某个 G 阻塞而停滞。 +> * **工作窃取**:当 P 的本地队列为空,会从其他 P 的队列**窃取一半 G**,实现负载均衡。 + +> **🎮 动手练一练:** +> 1. 运行 `runtime.GOMAXPROCS(0)` 查看当前 P 的数量。 +> 2. 尝试设置 `runtime.GOMAXPROCS(1)`,观察并发性能变化。 + +--- + +## 5.3 Channel —— Goroutine 之间的“传纸条” + +> **💡 想象一下:** +> 你有两个小助手(Goroutine),A 负责生产数据,B 负责消费数据。 +> 它们不能直接共享内存(会打架),怎么办? +> 于是,你放了一个**传送带**(Channel)在中间。A 把数据放上去,B 从上面拿走。 +> +> 这就是 **Channel**:**Goroutine 之间通信的管道**。 + +### 📖 Channel 的底层结构(图解) + +```mermaid +graph LR + subgraph "hchan 结构体" + qcount[qcount: 数据量] + dataqsiz[dataqsiz: 缓冲区大小] + buf[buf: 环形缓冲区] + sendx[sendx: 发送索引] + recvx[recvx: 接收索引] + sendq[sendq: 发送等待队列] + recvq[recvq: 接收等待队列] + 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 +``` + +> **📖 深度解析:** +> * **buf**:环形缓冲区,存储数据。 +> * **sendx/recvx**:发送/接收索引,环形移动。 +> * **sendq/recvq**:等待队列,存放阻塞的 Goroutine。 +> * **lock**:互斥锁,保证并发安全。 + +### 📝 Channel 的创建与使用 + +```go +// 1. 无缓冲 Channel (同步) +ch1 := make(chan int) +go func() { ch1 <- 42 }() +val := <-ch1 // 阻塞直到有数据 + +// 2. 有缓冲 Channel (异步) +ch2 := make(chan int, 2) +ch2 <- 1 +ch2 <- 2 +// ch2 <- 3 // 阻塞(缓冲区满) +val1 := <-ch2 +val2 := <-ch2 +``` + +> **💡 老师的小揭秘:** +> * **无缓冲**:发送和接收必须**同时就绪**(同步通信)。 +> * **有缓冲**:发送直到缓冲区满,接收直到缓冲区空(异步通信)。 +> * **关闭**:`close(ch)`,接收方用 `val, ok := <-ch` 判断是否关闭。 + +### ⚠️ 常见陷阱:忘记关闭 + +```go +func leak() { + ch := make(chan int) + go func() { + ch <- 1 + // 忘记关闭!接收方会一直阻塞 + }() + <-ch +} +``` + +> **💡 老师的小提醒:** +> * **谁发送,谁关闭**:通常由发送方关闭 Channel。 +> * **不要重复关闭**:会 panic。 +> * **不要向已关闭的 Channel 发送**:会 panic。 + +> **🎮 动手练一练:** +> 1. 创建一个无缓冲 Channel,启动一个 Goroutine 发送数据,主函数接收。 +> 2. 创建一个容量为 3 的 Channel,循环发送 5 个数据,观察阻塞行为。 + +--- + +## 5.4 同步原语 —— 避免“打架” + +> **🤔 多个 Goroutine 同时修改同一个变量,会发生什么?** +> 比如两个 Goroutine 同时 `count++`,结果可能少 1。这就是**竞态条件 (Race Condition)**。 +> +> 我们需要**同步原语**来保护共享资源。 + +### 📝 WaitGroup —— 等待所有任务完成 + +```go +var wg sync.WaitGroup + +for i := 0; i < 5; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + fmt.Println("任务", id, "完成") + }(i) +} + +wg.Wait() // 等待所有任务完成 +fmt.Println("所有任务完成") +``` + +> **💡 老师的小揭秘:** +> * `Add(n)`:增加计数器。 +> * `Done()`:减 1(通常用 `defer`)。 +> * `Wait()`:阻塞直到计数器为 0。 + +### 📝 Mutex —— 互斥锁 + +```go +var mu sync.Mutex +var count int + +go func() { + mu.Lock() + count++ + mu.Unlock() +}() +``` + +> **💡 老师的小提醒:** +> * **Lock/Unlock**:加锁/解锁。 +> * **死锁**:避免嵌套锁,保持锁顺序。 +> * **RWMutex**:读多写少时,用读写锁(`RLock`/`Unlock`)。 + +### 📝 Once —— 只执行一次 + +```go +var once sync.Once +func init() { + once.Do(func() { + fmt.Println("只执行一次") + }) +} +``` + +> **🎮 动手练一练:** +> 1. 用 `WaitGroup` 启动 10 个 Goroutine,统计它们的运行时间。 +> 2. 用 `Mutex` 保护一个共享计数器,验证并发安全。 + +--- + +## 5.5 Context —— 统一“叫停” + +> **💡 想象一下:** +> 老板(主函数)说:“停止所有任务!” +> 每个小助手(Goroutine)都要收到这个信号,并优雅地退出。 +> +> 这就是 **Context**:传递取消信号、超时控制。 + +### 📝 Context 的用法 + +```go +ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) +defer cancel() + +go func() { + select { + case <-time.After(3 * time.Second): + fmt.Println("任务完成") + case <-ctx.Done(): + fmt.Println("任务取消:", ctx.Err()) + } +}() + +time.Sleep(3 * time.Second) +``` + +> **💡 老师的小揭秘:** +> * `WithTimeout`:超时自动取消。 +> * `WithCancel`:手动取消。 +> * **树状传播**:父 Context 取消,所有子 Context 自动取消。 + +> **🎮 动手练一练:** +> 写一个函数,模拟耗时任务,用 Context 设置 1 秒超时,观察任务是否被取消。 + +--- + +## 5.6 本章小结 + +> **🎯 我们学到了什么?** +> 1. **Goroutine**:轻量级线程,`go` 启动。 +> 2. **GMP 模型**:G/M/P 调度,用户态切换,高效。 +> 3. **Channel**:通信管道,同步/异步,避免共享内存。 +> 4. **同步原语**:WaitGroup、Mutex、Once,保护共享资源。 +> 5. **Context**:取消信号,超时控制,树状传播。 +> +> **🚀 下一步预告:** +> 学会了并发,我们怎么把这些知识**应用到实际项目**中? +> +> **下一章,我们要构建一个完整的 Web API 服务!** 我们会用 Goroutine 处理请求,用 Channel 处理异步任务,用 Context 控制超时,用 Mutex 保护共享状态。准备好迎接**实战**了吗?