go runtime 不实现传统 m:n 调度,gomaxprocs 控制 p 数量而非 m:n 比例;调度由 schedule()、findrunnable()、execute() 三函数驱动,基于队列与状态机,无固定映射逻辑。

Go 的 runtime 并不实现传统意义的“M:N 调度”——它实际是 G(goroutine)在 M(OS 线程)上运行,由 P(逻辑处理器)协调调度,而 P 的数量默认等于 CPU 核数(GOMAXPROCS),M 数量动态伸缩。所谓“M:N”只是对调度规模关系的粗略描述,源码中根本不存在固定比例的 M:N 映射表或绑定逻辑。
为什么 grep runtime/src/runtime/proc.go 找不到 “M:N” 字样
Go 源码里从不使用 “M:N” 作为调度模型的正式术语。它只定义 struct g、struct m、struct p 和调度主循环 schedule()。所有调度决策都基于:当前 M 是否有绑定的 P、P 的本地队列是否为空、全局队列是否有待运行 G、是否存在可窃取的其他 P 队列——没有全局 N/M 比例控制逻辑。
-
GOMAXPROCS只控制P的初始数量,不是“N”的上限 -
M的创建由阻塞事件触发(如系统调用返回、netpoll唤醒),上限默认是 10000,但实际数量由负载驱动,不是预设配比 - 不存在任何代码去“维持 M:N 比例”,runtime 不感知、也不维护该抽象
真正决定调度行为的三个关键函数
看懂调度,必须盯住这三个函数入口,它们构成了所有 goroutine 切换的主干路径:
-
schedule():每个M进入调度循环的起点,负责找下一个可运行G;它先查gp->p->runq(本地队列),再查runtime.runq(全局队列),最后调findrunnable() -
findrunnable():核心查找逻辑,含工作窃取(runqsteal())、网络轮询(netpoll(false))、定时器检查;它返回一个可运行的G或进入休眠 -
execute():拿到G后真正跳转执行的地方,保存当前寄存器上下文到g->sched,然后jmp到G的entry
注意:execute() 不做任何“N 对 M 分配”计算,它只信任 findrunnable() 返回的 G 是合法且可运行的。
系统调用导致 M 脱离 P 时,G 怎么不丢
当 G 进入系统调用(比如 read()),对应 M 会脱离当前 P 并阻塞在内核态。此时该 G 并不会被销毁或丢失,而是:
- 被标记为
Gwaiting或Gsyscall状态 - 其栈和寄存器上下文完整保留在
g->sched中 -
P会被释放,供其他空闲M绑定并继续执行其他G - 系统调用返回后,原
M会尝试重新获取一个空闲P;若失败,则把该G放入全局队列,自己休眠
这个过程完全绕开了“M 必须对应固定 N 个 G”的假设——G 的生命周期独立于 M 存续时间,靠状态机 + 队列 + 全局链表(allg)管理。
真正容易被忽略的是:P 的数量不变,但 M 可能大量休眠或新建;G 的总数可以远超 M 或 P 的任意一方。调度器从不统计“当前用了几个 N”,它只确保每个可运行 G 最终都能落到某个 M+P 组合上执行——这才是源码里真实发生的调度。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











