Go的M休眠与唤醒不依赖操作系统信号,因其需避免系统调用开销、调度不确定性及干扰cgo;实际基于futex+park/unpark用户态协作机制,通过原子状态变更和notewakeup实现高效唤醒。

M 的休眠与唤醒不依赖操作系统信号(signal),而是基于 futex + 自旋 + park/unpark 原语的用户态协作机制。
为什么不用 sigwait/sigprocmask 这类系统信号?
Go runtime 要避免在 M 休眠/唤醒路径上触发系统调用开销,也必须绕过信号处理带来的调度不确定性(比如信号被投递到错误线程、需注册 handler、干扰 cgo 等)。实际实现中:
-
runtime.park_m最终调用ossemacquire,底层是futex(FUTEX_WAIT)(Linux)或WaitOnAddress(Windows) -
runtime.unpark对应ossemrelease,触发futex(FUTEX_WAKE)或WakeByAddressSingle - 所有 park/unpark 操作都发生在 M 绑定的 g0 栈上,不切换到信号 handler 上下文
- 被
runtime.LockOSThread()锁定的 M 会跳过 park 流程,直接进入阻塞等待,不参与 futex 等待队列
wakep 和 startm 怎么触发 M 唤醒?
这不是“发信号”,而是原子状态变更 + 条件唤醒。关键逻辑链如下:
-
ready(gp *g, ...)放入 G 后检查:atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 - 满足条件则调用
wakep(),它用atomic.Cas(&sched.nmspinning, 0, 1)尝试抢占 spinning 计数器 - CAS 成功后调用
startm(nil, true):若存在 idle M,则调用notewakeup(&m.park);否则新建 M -
notewakeup是 Go 封装的轻量级唤醒原语,本质仍是 futex wake 或类似系统调用,但不经过 signal 层
哪些情况会导致 M 无法被正常唤醒?
常见卡死或延迟唤醒现象,往往源于以下几类边界条件:
- cgo 调用中派生的 M:由 C 侧创建,Go runtime 不持有其控制权,
notewakeup对它无效,也不会被startm管理 - M 正在执行系统调用且未调用
entersyscall/exitsyscall:此时 M 处于_Msyscall状态,不响应 park,也不计入sched.nmidle - P 被 handoff 到新 M 但旧 M 仍持有部分资源(如 netpoller fd):可能造成虚假 idle 判断,导致唤醒滞后
- 频繁的
GOMAXPROCS动态调整:会重置sched.npidle和sched.nmspinning,干扰 wakep 的 CAS 判断节奏
调试 M 休眠/唤醒问题时该看什么?
别查 strace 里的 sigwait 或 kill,重点盯住这几个 runtime 内部状态:
- 用
pprof.Lookup("goroutine").WriteTo(w, 1)查看 M 是否处于GC sweep wait、chan receive等可 park 状态 - 观察
runtime.MemStats.MCacheInuse和runtime.NumThread()变化趋势,确认 M 是否真被回收而非卡在 park 中 - 开启
GODEBUG=schedtrace=1000,关注输出中idle、spinning、runnable的数值跳变是否符合预期 - 注意
m.spinning字段:为 true 表示该 M 正在主动偷 G,此时不会被 park;为 false 且m.p == nil才可能进入休眠
真正难定位的不是“M 没唤醒”,而是“M 被唤醒了,但没立刻绑定 P 或没拿到 G”——这中间隔着 work-stealing 尝试、netpoller 回调、以及全局队列批量迁移的延迟窗口。


















