Goroutine 与 GMP
goroutine 是 Go 并发的核心:几 KB 起步的轻量线程,十万级无压力。面试必考 GMP 模型。
goroutine 为什么轻
| 维度 | goroutine | 线程 |
|---|---|---|
| 栈 | 2KB 起步,动态增长 | MB 级固定 |
| 创建开销 | ~1-2µs | 数十 µs(内核调用) |
| 切换 | 用户态(自己调度) | 内核态 |
| 规模 | 十万级 | 千级 |
栈动态增长是关键:初始 2KB,按需增长(最大 1GB),goroutine 少时内存占用极小。
GMP 模型
GMP: G 是任务, M 是执行者, P 是调度器
| 角色 | 含义 |
|---|---|
| G(Goroutine) | 用户态任务:栈 + 上下文 |
| M(Machine) | 内核线程,真正执行 G |
| P(Processor) | 调度器:本地 G 队列,默认 = CPU 核数 |
调度要点:
- P 的数量 = GOMAXPROCS(默认 CPU 核数),每 P 一个本地队列
- 创建 G 进本地队列;队列满进全局队列
- work stealing:P 空闲时从别的 P 偷一半 G(负载均衡)
- 阻塞系统调用:M 让出 P(P 挂到别的 M 上继续调度),G 等系统调用返回
- 抢占:Go 1.14+ 异步抢占(信号),长时间运行的 G 会被强制让出(10ms 级),防止饿死其他 G
goroutine 泄漏 ★
goroutine 泄漏:G 永远阻塞,栈和资源不释放,积累导致内存耗尽。常见场景:
| 场景 | 解法 |
|---|---|
| 从 channel 读,无人写 | 超时(select + time.After) |
| 向无缓冲 channel 写,无人读 | 带缓冲或超时 |
| 死循环无退出条件 | 检查退出标志(Context) |
| 忘记关 channel | 确定生产者/消费者职责 |
select {
case v := <-ch:
// 正常
case <-time.After(3 * time.Second):
// 超时退出, 不泄漏
}面试话术:goroutine 泄漏 = 阻塞没出口。排查:pprof goroutine profile 看堆积的函数(见标准库篇)。
面试追问
- GMP 是什么? G 任务、M 内核线程、P 调度器(本地队列)。P 数 = CPU 核数
- goroutine 为什么轻? 栈 2KB 动态增长 + 用户态调度(无内核切换)。十万级无压力
- work stealing? P 空闲时偷别的 P 的 G,负载均衡。配合全局队列
- 怎么抢占? 1.14+ 异步抢占(信号),长时间运行的 G 强制让出。防饿死
- goroutine 泄漏怎么排查? 阻塞没出口(channel 等无人、死循环)。pprof goroutine profile 定位