Go GMP 调度模型详解

Go GMP 调度模型详解

一、核心概念

GMP 是 Go runtime 的三大核心组件:

组件 全称 本质
G Goroutine 协程,包含栈、指令指针、调度信息等,初始栈 2KB
M Machine OS 线程,实际执行者,负责从 P 获取 G 并执行
P Processor 逻辑处理器,持有本地 runqueue(LRQ),默认数量 = CPU 核数

核心关系:

复制代码
M1 ──┐        M2 ──┐
     P1            P2
   ┌─────┐      ┌─────┐
   │ LRQ │      │ LRQ │  ← 本地队列(无锁环形队列,最多256个G)
   ├─────┤      ├─────┤
   │ G   │      │ G   │
   │ G   │      │ G   │
   │ ... │      │ ... │
   └─────┘      └─────┘
      \            /
       \          /
      ┌──────────────┐
      │  GRQ(全局队列)│  ← 所有P共享,LRQ溢出时存入
      └──────────────┘

二、调度原理

1. 调度循环(核心)

每个 M 在绑定的 P 上不断执行:

复制代码
for {
    // 1. 检查定时器
    // 2. 从 LRQ 获取 G(work-stealing 优先)
    // 3. LRQ 空 → 从 GRQ 获取(batch取,最多一半)
    // 4. GRQ 也空 → 从其他 P 的 LRQ 偷取(work-stealing)
    // 5. 都没有 → 执行 netpoll(检查网络IO就绪的G)
    // 6. 还没有 → 释放 P,进入休眠
}

2. 四种调度场景

① 创建新 G

  • 当前 P 的 LRQ 未满 → 放入 LRQ(本地队列,无锁)
  • LRQ 已满(256个)→ 将前一半移到 GRQ,新 G 放入 LRQ

② G 阻塞(syscall/网络IO)

  • 系统调用阻塞:M 解绑 P,P 转交给其他空闲 M;如果没有空闲 M 且有其他 G 等待,创建新 M
  • 网络IO:G 不阻塞 M,而是注册到 netpoller,M 继续执行其他 G;IO 就绪后 netpoll 唤醒 G 重新入队

③ G 执行完毕

  • 递归调用 schedule(),获取下一个 G

④ 抢占式调度(Go 1.14+)

  • 基于信号的协作式抢占改为基于信号的异步抢占
  • runtime 在栈帧中插入 morestack 检查点
  • 运行时间超过 GOMAXPROCS*10ms 的 G 会被标记为可抢占
  • 发送 SIGURG 信号,在 signal handler 中完成栈分裂和抢占

3. Work-Stealing 机制

当 P 的 LRQ 为空时:

复制代码
P1 (空)  ──偷取──→  P2 (有32个G)
  │
  └─ 每次偷取一半,保证负载均衡
  • 从随机一个 P 开始尝试,避免集中竞争
  • 偷取数量 = 目标 LRQ 长度 / 2,最少偷 1 个
  • 这是 GMP 高效的关键:局部性优先(LRQ无锁),全局均衡(stealing)

4. Hand Off 机制

复制代码
G1 发起阻塞 syscall
  ↓
M1 解绑 P1,P1 进入 idle list
  ↓
如果存在 idle M → M_new 绑定 P1 继续调度
如果不存在 idle M → 创建新 M
  ↓
M1 的 syscall 返回
  ↓
M1 尝试获取空闲 P → 成功则恢复执行
                   → 失败则 G1 放入全局队列,M1 休眠

三、关键参数

参数 默认值 说明
GOMAXPROCS CPU 核数 P 的数量,决定并行度
GODEBUG=schedtrace=1000 - 每1000ms打印调度信息
runtime.GOMAXPROCS(n) - 动态调整P数量

核心结论:P 数量决定了能同时执行的 G 数量上限。 GOMAXPROCS=1 时所有 G 串行执行。

四、常见用法

1. 控制并行度

go 复制代码
// CPU 密集型:GOMAXPROCS 保持默认(= 核数)最佳
// IO 密集型:可以适当增大,但意义不大,因为IO不占M
runtime.GOMAXPROCS(runtime.NumCPU())

2. 协程池模式(控制并发)

go 复制代码
// 用 buffered channel 做信号量
sem := make(chan struct{}, 100) // 最多100个并发goroutine

for i := 0; i < 10000; i++ {
    sem <- struct{}{}
    go func(id int) {
        defer func() { <-sem }()
        doWork(id)
    }(i)
}

3. 观察调度状态

go 复制代码
// 开启调度追踪
_ = os.Setenv("GODEBUG", "schedtrace=1000,scheddetail=1")

输出示例:

复制代码
SCHED 1000ms: gomaxprocs=8 idleprocs=0 threads=20 spinningthreads=0 idlethreads=12 runqueue=0 [0 0 0 0 0 0 0 0]

4. 用 runtime 包获取调度信息

go 复制代码
fmt.Println("Goroutines:", runtime.NumGoroutine())
fmt.Println("Threads:", runtime.NumThread()) // 不是公开API,但可通过 /debug/pprof/ 查看

五、常见问题与排查

1. Goroutine 泄漏

症状: runtime.NumGoroutine() 持续增长不回落

常见原因:

go 复制代码
// ❌ channel 无消费者,goroutine 永久阻塞在发送
ch := make(chan int)
go func() { ch <- 1 }()

// ❌ channel 无生产者,goroutine 永久阻塞在接收
ch := make(chan int)
go func() { <-ch }()

// ❌ select 中所有 case 都阻塞,没有 default
go func() {
    select {
    case <-ch1:
    case <-ch2:
    }
}()

排查工具:

bash 复制代码
go tool pprof http://localhost:6060/debug/pprof/goroutine

2. 线程数暴增

症状: top 看到 Go 进程线程数远超预期

原因: 大量 goroutine 同时做阻塞式系统调用(cgo、文件IO),每个都会 Hand Off 导致创建新 M

解决方案:

  • 避免高并发 cgo 调用
  • 文件 IO 考虑用线程池限制
  • debug.SetMaxThreads(max) 设置上限

3. 调度延迟 / 抢占不及时(Go < 1.14)

症状: 某个 G 长时间占用 M,其他 G 饿死

go 复制代码
// Go 1.13 及以下的问题:for 循环中没有函数调用,不会触发栈检查
for {
    // 紧密循环,不会被抢占(Go < 1.14)
}

// 修复:显式调用 runtime.Gosched() 让出时间片
for {
    runtime.Gosched()
}

Go 1.14+ 已解决: 基于信号的异步抢占,无需手动 Gosched()

4. STW(Stop The World)过长

原因:

  • GC 触发时需要暂停所有 M
  • 大量 goroutine 导致栈扫描时间长

排查:

go 复制代码
import _ "runtime/pprof"
// 访问 /debug/gctrace

5. Sysmon 后台监控

runtime 有一个独立的 M 运行 sysmon(不绑定 P),负责:

  • 抢占长时间运行的 G(retake
  • 回收空闲的 P 和 M
  • 触发 GC(超过 2 分钟没有 GC)
  • 处理定时器

六、GMP vs 其他模型对比

特性 GMP 协作式调度 多线程模型
创建成本 极低(2KB) 高(MB级栈)
切换成本 几十ns 几十ns 几μs
调度复杂度 高(三层抽象)
系统调用处理 Hand Off 阻塞线程 阻塞线程
可扩展性 百万级 万级 千级

七、一句话总结

GMP 的本质是用 P 做中间层解耦了 G 和 M:G 无感知 M 的阻塞/创建/销毁,P 做本地缓存实现无锁调度,Work-Stealing 保证负载均衡,Hand Off 保证 syscall 不饿死其他 G。

相关推荐
晓杰'1 小时前
从0到1实现 Balatro 游戏后端(1):项目规划与牌型判断实现
后端·websocket·typescript·node.js·游戏开发·项目实战·nestjs
2401_878454531 小时前
js的复习(一)
开发语言·javascript·ecmascript
旺仔老馒头.1 小时前
【C++】类和对象(二)
开发语言·c++·后端·类和对象
等故意1 小时前
C# 工业视觉上位机开发心得分享
开发语言·数码相机·c#·视觉检测
广师大-Wzx1 小时前
JavaWeb:后端部分
java·开发语言·spring·servlet·tomcat·maven·mybatis
机器学习之心1 小时前
基于CPO-VMD冠豪猪优化优化变分模态分解与最小包络熵的自适应变分模态分解方法,附MATLAB代码
开发语言·matlab·cpo-vmd·冠豪猪优化优化变分模态分解
广东王多鱼1 小时前
一个人 + Claude = 全栈开发团队:从零构建 AI 自动化开发系统的技术实现
后端·vibecoding
用户2160719532951 小时前
AQS、ReentrantLock详解
后端
lly2024061 小时前
Font Awesome 文件类型图标
开发语言