空闲m休眠是因无g可执行且无gc/sysmon任务,为节省cpu而挂起在park上;唤醒由newproc1、netpoll、exitsyscall等事件通过ready函数触发,需有空闲p配合。

空闲 M 为什么会休眠?
Go 调度器不会让 M(内核线程)长期空转等待任务。当一个 M 找不到可运行的 G(比如本地 P 队列、全局队列、netpoller、其他 P 的队列都为空),且当前没有 GC 或 sysmon 等串行任务需要它参与时,它就会进入休眠状态——调用 notesleep 等待被唤醒。
关键判断逻辑在 schedule 函数末尾:如果经过「强力查找」仍没拿到 G,且 m.spinning == false,就执行 stopm;而 stopm 最终会把 M 挂起在 m.park 上,等待 ready 或 notewakeup。
- 休眠前会把 M 放入全局空闲 M 列表
runtime.allm中的空闲链表(sched.midle) - 休眠时 M 不再占用 CPU,但其栈和寄存器上下文仍保留在内存中
- 注意:M 休眠 ≠ 线程销毁,Go 尽量复用已有 M,避免频繁
clone系统调用开销
哪些场景会唤醒空闲 M?
唤醒不是随机的,而是由明确的「有新 G 可运行」事件触发,核心入口是 ready 函数。
- 新 goroutine 启动(
newproc1):若当前 P 本地队列已满或 M 正忙,且空闲 M 存在,就从sched.midle取一个 M 并notewakeup(&m.park) - 网络 I/O 就绪(
netpoll返回 G):轮询器发现 socket 可读/可写后,调用injectglist把 G 推入本地队列,并尝试唤醒空闲 M(startm) - G 从系统调用返回(
exitsyscall):若该 G 原绑定的 M 已被抢占或休眠,且 P 可用,则可能唤醒空闲 M 来接管 - GC 标记阶段结束、STW 恢复时:调度器会批量唤醒部分 M 以加速并发标记
唤醒动作本身不保证 M 立即执行——它只是解除 notesleep 阻塞,M 醒来后仍要重新走一遍 schedule 流程抢 G。
为什么有时唤醒失败或延迟明显?
常见现象是:明明有 G 入队,但 CPU 使用率掉到 0%,几毫秒后才恢复。这通常不是 bug,而是设计权衡的结果。
-
startm在唤醒前会检查sched.nmspinning > 0:如果已有 M 在自旋找 G,它会放弃唤醒,避免过多线程争抢导致 cache line bouncing - 空闲 M 被唤醒后,第一件事是尝试获取 P;若所有 P 都被占用(
sched.pidle为空),它会立刻再次休眠——所以 P 数量(GOMAXPROCS)不足时,唤醒等于白唤 - 系统调用阻塞期间,M 脱离 P,此时即使有 G 就绪,也得等该 M 完成系统调用并调用
exitsyscall,或由其他 M 抢走 P 再唤醒新 M - Linux 下
futex唤醒存在微小延迟,尤其在高负载时;Go 1.21+ 引入了asyncpreemptoff调试开关,可用于排除抢占干扰
如何验证当前空闲 M 状态?
不依赖 pprof 或 trace,直接看运行时调试输出最准:
go run -gcflags="-l" -ldflags="-s -w" main.go & GODEBUG=schedtrace=1000,scheddetail=1
输出中重点关注:
-
Mxx: idle行表示该 M 当前休眠 -
SCHED行里的idleprocs=和idlems=是实时计数 - 若
idlems > 0但runqueue=0,说明调度器确实无事可做;若runqueue > 0却长时间idlems > 0,大概率是 P 绑定问题或 sysmon 被卡住
真正容易被忽略的是:M 的休眠/唤醒不是孤立行为,它始终与 P 的可用性、G 的就绪路径(尤其是 netpoller 是否正常注册)、以及 sysmon 是否健康强耦合——单独调优 M 数量往往无效,得一起看三者联动。











