# Goroutine 和并发模型 ## 问题 1. 什么是 Goroutine?与线程的区别? 2. 如何优雅地关闭 Goroutine? 3. Channel 的底层实现原理是什么? 4. select 的作用是什么? 5. context 包的作用是什么? 6. 如何避免 Goroutine 泄漏? 7. Go 的调度器(GMP 模型)是怎样的? --- ## 标准答案 ### 1. Goroutine vs 线程 #### **对比** | 特性 | Goroutine | 线程 | | -------- | ----------- | ------ | | **内存占用** | 2 KB(初始) | 1-2 MB | | **创建成本** | 极低 | 较高 | | **切换成本** | 低(用户态) | 高(内核态) | | **数量** | 数十万+ | 数千个 | | **调度** | Go 调度器(GMP) | 操作系统 | | **栈** | 动态伸缩(按需增长) | 固定大小 | --- #### **创建 Goroutine** ```go func hello(name string) { fmt.Println("Hello,", name) } func main() { // 启动 Goroutine go hello("Alice") go hello("Bob") // 等待(防止主 Goroutine 退出) time.Sleep(time.Second) } ``` --- ### 2. 优雅关闭 Goroutine #### **问题示例** ```go // ❌ 无法停止 func worker() { for { fmt.Println("working...") time.Sleep(time.Second) } } func main() { go worker() time.Sleep(3 * time.Second) // 程序退出,worker 未清理 } ``` --- #### **解决方案:使用 channel** ```go func worker(stop <-chan struct{}) { for { select { case <-stop: fmt.Println("worker stopping...") return default: fmt.Println("working...") time.Sleep(time.Second) } } } func main() { stop := make(chan struct{}) go worker(stop) time.Sleep(3 * time.Second) close(stop) // 发送停止信号 time.Sleep(time.Second) } ``` --- ### 3. Channel 底层实现 #### **Channel 结构** ```go type hchan struct { qcount uint // 队列中的数据量 dataqsiz uint // 环形队列大小 buf unsafe.Pointer // 环形队列指针 elemsize uint16 // 元素大小 closed uint32 // 是否关闭 elemtype *_type // 元素类型 sendx uint64 // 发送索引 recvx uint64 // 接收索引 recvq waitq // 接收等待队列 sendq waitq // 发送等待队列 lock mutex // 互斥锁 } ``` --- #### **Channel 类型** **1. 无缓冲 Channel**: ```go ch := make(chan int) // 发送会阻塞,直到有接收者 go func() { ch <- 1 // 阻塞 }() <-ch // 接收 ``` **2. 有缓冲 Channel**: ```go ch := make(chan int, 10) // 缓冲大小 10 // 缓冲未满时,发送不阻塞 ch <- 1 ch <- 2 ``` --- ### 4. select 的作用 #### **多路复用** ```go func worker1(ch chan<- int) { time.Sleep(1 * time.Second) ch <- 1 } func worker2(ch chan<- int) { time.Sleep(2 * time.Second) ch <- 2 } func main() { ch1 := make(chan int) ch2 := make(chan int) go worker1(ch1) go worker2(ch2) select { case val := <-ch1: fmt.Println("worker1:", val) case val := <-ch2: fmt.Println("worker2:", val) case <-time.After(3 * time.Second): fmt.Println("timeout") } } ``` --- #### **select 的特性** 1. **随机选择**:多个 case 就绪时随机选择 2. **阻塞**:所有 case 不就绪时阻塞 3. **default**:添加 default 后,select 不阻塞 ```go ch := make(chan int) select { case val := <-ch: fmt.Println(val) default: fmt.Println("no data") } ``` --- ### 5. context 包 #### **作用** 1. **超时控制** 2. **取消信号传递** 3. **元数据传递** --- #### **使用示例** **1. 超时控制**: ```go func doWork(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("work cancelled:", ctx.Err()) return default: fmt.Println("working...") time.Sleep(500 * time.Millisecond) } } } func main() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() go doWork(ctx) time.Sleep(3 * time.Second) } ``` --- **2. 取消信号**: ```go func main() { ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(1 * time.Second) cancel() // 取消 }() select { case <-ctx.Done(): fmt.Println("cancelled:", ctx.Err()) } } ``` --- **3. 元数据传递**: ```go func handler(ctx context.Context) { userID := ctx.Value("userID").(string) fmt.Println("userID:", userID) } func main() { ctx := context.WithValue(context.Background(), "userID", "12345") handler(ctx) } ``` --- ### 6. 避免 Goroutine 泄漏 #### **问题示例** ```go // ❌ Goroutine 泄漏 func query() { ch := make(chan int) go func() { val := <-ch // 永久阻塞 fmt.Println(val) }() // 如果没有数据发送到 ch,Goroutine 永久泄漏 } ``` --- #### **解决方案** **1. 使用 context**: ```go func query(ctx context.Context) { ch := make(chan int) go func() { select { case val := <-ch: fmt.Println(val) case <-ctx.Done(): return // 超时或取消时退出 } }() select { case ch <- 1: case <-time.After(1 * time.Second): fmt.Println("timeout") } } ``` --- **2. 使用缓冲 Channel**: ```go func query() { ch := make(chan int, 1) // 缓冲大小 1 go func() { val := <-ch fmt.Println(val) }() ch <- 1 // 不会阻塞 } ``` --- ### 7. GMP 调度模型 #### **组成** - **G(Goroutine)**:用户级线程(轻量级) - **M(Machine)**:系统线程(与内核线程 1:1) - **P(Processor)**:逻辑处理器(M 的队列,运行 Go 代码) --- #### **GMP 模型** ``` P1 (Processor) P2 (Processor) P3 (Processor) │ │ │ ├─ G1 ├─ G5 ├─ G9 ├─ G2 ├─ G6 ├─ G10 └─ G3 └─ G7 └─ G11 │ │ │ ↓ ↓ ↓ M1 (Thread) M2 (Thread) M3 (Thread) ``` **工作流程**: 1. P 维护一个 Goroutine 队列 2. P 从队列中取出 G,执行 3. G 阻塞(I/O、channel)时,P 继续执行其他 G 4. P 空闲时,从其他 P "偷" G(工作窃取) --- #### **调度策略** 1. **Work-Stealing(工作窃取)**:P 从其他 P 窃取 G 2. **抢占式调度**:sysmon 线程抢占长时间运行的 G 3. **Time Slice(时间片)**:每个 G 执行 10ms 后调度 --- ### 8. 阿里 P7 加分项 **深度理解**: - 理解 Go 调度器的实现原理(GMP 模型) - 理解 Channel 的底层实现(环形队列、锁) - 理解 context 的传播机制 **实战经验**: - 有处理 Goroutine 泄漏的经验 - 有处理并发竞争条件的经验 - 有性能优化的经验(减少锁、使用 sync.Pool) **性能优化**: - 理解如何避免 Goroutine 泄漏 - 理解如何减少 Channel 阻塞 - 理解如何提高并发性能(Worker Pool、批处理)