Go Goroutine 与 GMP 调度模型
Go 的并发模型是其最强大的特性之一。理解 GMP 调度模型对于写出高性能 Go 程序至关重要。
Goroutine 基础
什么是 Goroutine
Goroutine 是 Go 轻量级的协程,由 Go 运行时管理:
go// 启动一个 goroutine go func() { fmt.Println("Hello from goroutine") }() // 普通函数也可以用 go 启动 go doSomething()
Goroutine vs 线程
| 特性 | 线程 | Goroutine |
|---|---|---|
| 创建成本 | ~1MB 栈 | ~2KB 栈(可动态增长) |
| 创建速度 | 慢 | 极快 |
| 调度 | 操作系统(内核态) | Go 运行时(用户态) |
| 切换成本 | 用户态→内核态 | 用户态内切换 |
Goroutine 的栈空间
Goroutine 的栈是动态增长的:
gofunc recursion() { // 初始栈大小约 2KB // 当栈空间不足时,会自动扩容(最多 1GB) recursion() }
GMP 调度模型
GMP 架构
┌─────────────────┐
│ Goroutines │
│ (待执行的 Go) │
└────────┬──────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ OS (Operating System) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ M (Machine/Thread) │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ P (Processor) │ │ │
│ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │
│ │ │ │ G 1 │ │ G 2 │ │ G 3 │ ... │ │ │
│ │ │ │(gorout.)│ │(gorout.)│ │(gorout.)│ │ │ │
│ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │
│ │ │ 本地运行队列 (LRQ) │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
▲ ▲
│ │
└──────── 全局运行队列 (GRQ) ─────────┘
- G (Goroutine):Go 协程,包含执行栈和调度信息
- M (Machine):操作系统线程,实际执行代码
- P (Processor):逻辑处理器,有本地运行队列
GMP 工作流程
go// 启动 Go 程序 func main() { // 1. 创建 P(默认 GOMAXPROCS 个) // 2. 创建 M(内核线程) // 3. 创建 main goroutine 在 P 上运行 for i := 0; i < 10; i++ { go fmt.Println(i) // 创建新 goroutine,加入 P 的本地队列 } time.Sleep(time.Second) }
调度过程
- M 从本地队列获取 G:
M → P → G - 本地队列为空时:从全局队列或其他 P 偷取
- G 阻塞时:M 释放 P,寻找其他 G 执行
- G 完成后:放回队列或销毁
关键调度机制
Work Stealing(工作窃取)
当 P 的本地队列为空时,从其他 P 偷取一半 G:
go// 源码简化逻辑 func (p *Processor) run() { for { if p.lrq.size > 0 { g := p.lrq.pop() execute(g) } else { // 尝试从全局队列获取 g := globalRunQueue.pop() if g != nil { execute(g) } else { // 从其他 P 偷取 g = stealFromOtherP() } } } }
Hand off(移交)
当 M 阻塞时(如系统调用),释放 P 给其他 M:
G1 执行中 → 发起系统调用 → M1 阻塞
↓
M2 接管 P
Goroutine 阻塞分类
| 阻塞类型 | M 是否释放 P | 说明 |
|---|---|---|
| 系统调用 | 是 | 网络 I/O、文件 I/O |
| channel 操作 | 否 | 等待数据 |
| mutex 等待 | 否 | 等待锁 |
| time.Sleep | 否 | 休眠 |
channel 与调度
channel 的阻塞行为
go// 发送阻塞:队列满时 ch := make(chan int, 3) for i := 0; i < 100; i++ { ch <- i // 超过3个会阻塞 } // 接收阻塞:队列空时 data := <-ch // 无数据时阻塞
select 与调度
goselect { case v := <-ch1: fmt.Println("收到 ch1:", v) case v := <-ch2: fmt.Println("收到 ch2:", v) case <-time.After(time.Second): fmt.Println("超时") default: fmt.Println("默认分支") }
常见面试问题
Q1: GMP 模型中 P 的数量是多少?
默认等于 CPU 核心数:
gofunc main() { // 查看 CPU 核心数 fmt.Println(runtime.NumCPU()) // 如:8 // 查看默认 P 数量 fmt.Println(runtime.GOMAXPROCS(0)) // 默认等于 NumCPU // 动态修改 P 数量(不建议在生产环境修改) runtime.GOMAXPROCS(4) }
Q2: Goroutine 泄漏如何排查?
Goroutine 泄漏通常由 channel 阻塞引起:
go// 泄漏示例 func leak() { ch := make(chan int) go func() { ch <- 42 // 发送后永不接收 }() return // leak() 返回后,goroutine 永远阻塞 } // 解决:使用 context 或 select func noLeak(ctx context.Context) { ch := make(chan int) go func() { select { case ch <- 42: case <-ctx.Done(): return } }() return }
Q3: 如何控制并发数量?
使用 worker pool 模式:
gofunc workerPool(n int) { ch := make(chan int, 100) // 启动 n 个 worker var wg sync.WaitGroup for i := 0; i < n; i++ { wg.Add(1) go func(id int) { defer wg.Done() for task := range ch { process(task) } }(i) } // 发送任务 for i := 0; i < 1000; i++ { ch <- i } close(ch) wg.Wait() }
Q4: runtime.Gosched() 的作用?
主动让出 CPU,允许其他 goroutine 执行:
gofunc main() { go func() { for i := 0; i < 5; i++ { fmt.Print("A") runtime.Gosched() // 让出执行权 } }() for i := 0; i < 5; i++ { fmt.Print("B") runtime.Gosched() } time.Sleep(time.Second) }
性能优化建议
- 避免大量创建 Goroutine:使用 worker pool
- 合理设置 GOMAXPROCS:通常默认即可
- 避免 shared memory 通信:使用 channel
- 注意 channel 容量:减少阻塞
- 使用 sync.Pool:复用临时对象
go// 复用对象,减少 GC 压力 var bufPool = sync.Pool{ New: func() interface{} { b := make([]byte, 1024) return &b }, } func process() { buf := bufPool.Get().(*[]byte) defer bufPool.Put(buf) // 使用 buf }