# 第五章:并发编程 —— 让程序“多任务”并行 > **👋 回顾一下:** > > 前四章,我们学会了基础语法、数据结构、函数和接口。现在,你已经能写出逻辑清晰、结构良好的代码了。 > > **🤔 但是,现实世界的任务往往很复杂……** > > 想象一下,你是一家餐厅的老板。 > * 如果只有一个厨师(单线程),客人点菜 -> 厨师炒菜 -> 上菜。客人要等很久。 > * 如果有十个厨师(多线程/多协程),可以**同时**处理十张订单,效率翻倍! > > 这就是**并发 (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 保护共享状态。准备好迎接**实战**了吗?