Go GMP 调度器
一、GMP 到底是什么?
- G (Goroutine) :用户轻量级线程,包含栈、指令、状态,很小(2KB 起) 。
- P (Logical Processor) :逻辑处理器,承载 G,连接 M 与 G,必须有 P 才能运行 G。
- M (Machine) :系统线程(OS Thread) ,真正被 CPU 执行的载体,绑定 P 才能跑 G。
口诀
M 必须绑定 P 才能运行 G
P 持有 G 队列
M 从 P 拿 G 执行
P 数量 = GOMAXPROCS(真正并行度)
二、GMP 核心结构体
文件:runtime/runtime2.go
1. G (Goroutine)
type g struct {
stack stack // goroutine 栈:栈顶、栈底
sched gobuf // 调度上下文:保存寄存器(PC、SP、BP等)
goid int64 // 协程ID
status uint32 // 状态:Gidle/Grunnable/Grunning/Gsyscall/Gwaiting...
waitreason string // 等待原因
m *m // 当前绑定的M(正在哪个M上跑)
p *p // 当前绑定的P
}
// 调度上下文(切换时保存CPU现场)
type gobuf struct {
sp uintptr // 栈指针
pc uintptr // 程序计数器
ret uintptr // 返回值
...
}
2. P (Logical Processor)
type p struct {
m *m // 当前绑定的M
status uint32 // 状态:Pidle/Prunning/Psyscall/Pgcstop
runq [256]guintptr // 本地G队列(无锁,极快)
runqhead uint32 // 队列头
runqtail uint32 // 队列尾
runqsize int32 // 本地队列大小
gfree *g // G缓存池(复用G,减少分配)
}
3. M (Machine/OS Thread)
type m struct {
g0 *g // 系统栈G(调度、系统调用专用)
curg *g // 当前正在运行的用户G
p *p // 绑定的P
nextp *p // 即将绑定的P
mOS mOS // 系统线程相关信息
lockedg *g // 被锁定的G(LockOSThread)
}
4. 全局调度器(sched)
var sched struct {
mutex mutex // 全局锁
globalRunq gQueue // 全局G队列
pidle []*p // 空闲P队列
nmidle int32 // 空闲M数量
nmrunning int32 // 运行中M数量
}
三、GMP 核心状态
G 状态
- Gidle:空闲
- Grunnable:可运行(等待 P 执行)
- Grunning:正在运行
- Gsyscall:系统调用中
- Gwaiting:等待(锁 /chan)
P 状态
- Pidle:空闲
- Prunning:运行中
- Psyscall:系统调用
- Pgcstop:GC 停止
四、GMP 核心机制
1. 多级队列调度
- P 本地 runq:无锁,最快
- 全局 runq:有锁,备用
- netpoller:异步 IO 就绪 G
2. work stealing(偷任务)
当 P 本地队列为空,去别的 P 偷一半 G来执行。
3. hand off(P 剥离)
G 进入系统调用 → M 与 P 剥离 → P 可以被其他 M 绑定继续执行 G。
4. 抢占调度(Go 1.14+ 异步抢占)
防止 G 长期占用 CPU,每 10ms触发一次抢占。
五、核心调度源码(逐行超详细注解)
文件:runtime/proc.go
1. 调度主循环:schedule () —— 灵魂函数
// schedule 调度主循环:M 不断寻找 G 执行
func schedule() {
// 死循环:M永不退出
for {
// 1. 禁止GC抢占
mp := acquirem()
// 2. 尝试从 P 本地队列取 G(最快,无锁)
if gp, inheritTime := runqget(mp.p); gp != nil {
// 本地有G → 直接执行
execute(gp, inheritTime)
}
// 3. 尝试从 全局队列 取 G
if gp := globrunqget(mp.p, 1); gp != nil {
execute(gp, false)
}
// 4. 尝试从 netpoller 取IO就绪G
if gp := netpoll(false); gp != nil {
execute(gp, false)
}
// 5. 偷任务:从其他P偷G(work-stealing)
if gp := findrunnable(); gp != nil {
execute(gp, false)
}
// 6. 没找到G → M进入idle,释放P
mput(mp)
}
}
2. 执行 G:execute ()
// execute 真正开始运行G
func execute(gp *g, inheritTime bool) {
// M绑定G
mp.curg = gp
// G状态改为运行中
gp.status = Grunning
// 恢复调度上下文(PC/SP)
gogo(&gp.sched)
}
3. G 切换:gogo () —— 汇编实现(保存 / 恢复 CPU 现场)
TEXT runtime·gogo(SB), NOSPLIT, $0-8
MOVQ buf+0(FP), DX // DX = &gp.sched
MOVQ gobuf_sp(DX), SP // 恢复栈指针 SP
MOVQ gobuf_pc(DX), PC // 恢复程序计数器 PC → 跳转到G执行
4. 主动让出:Gosched ()
func Gosched() {
// 保存当前G上下文
m.pcbuf = getcallerpc()
// 让出P,进入Grunnable
runtime.Gosched()
}
5. 系统调用剥离 P:entersyscall ()
func entersyscall() {
// 系统调用开始 → M与P剥离
releasem(mp)
// P状态改为Psyscall
mp.p.status = Psyscall
}
6. 异步抢占(10ms)
func preemptMS() {
// 每10ms给运行中的G发送抢占信号
// 让G在函数序言时主动让出CPU
}
六、GMP 全流程流程图
1. G 创建 → 调度 → 执行 完整流程
2. Work Stealing 偷任务流程
3. 系统调用 Hand Off 流程
4. 异步抢占调度流程
七、GMP 10 个细节
1. 真正并行度 = P 数量(GOMAXPROCS)
M 可以很多,但只有 P 能让 G 并行。
2. P 本地队列无锁,全局队列有锁
所以优先跑本地队列。
3. G 不直接绑定 OS 线程,M 才是
G 是轻量级,M 是重量级。
4. M 可以比 P 多
当阻塞时,调度器会创建新 M 绑定 P。
5. Work Stealing 是 Go 高并发核心
让所有 CPU 满载。
6. 系统调用不会阻塞 P
P 会被剥离,继续服务其他 M。
7. Go 1.14+ 支持基于信号的异步抢占
解决死循环霸占 CPU 问题。
8. G 栈极小(2KB),可动态扩容
所以能百万并发。
9. g0 栈是系统栈
调度、GC 都在 g0 栈执行。
10. 全局队列只是兜底
绝大多数 G 永远走本地队列。
八、总结
- G:任务
- P:队列 + 资源
- M:执行载体
- M 必须绑定 P 才能跑 G
- P 本地队列无锁最快
- 偷任务保证 CPU 满载
- 系统调用自动剥离 P
- 抢占防止长期占用