findrunnable在找不到G时先进入自旋而非立即休眠,是为了避免调度延迟——新G可能刚由netpoll、syscall或定时器唤醒,短暂自旋可及时捕获,比休眠再唤醒开销更小;其自旋受_g_.m.spinning为true且存在非idle P双重条件控制,任一不满足即跳转stop进入休眠。
findrunnable 为什么会在找不到 G 时进入自旋而不是立刻休眠
因为立刻休眠会带来调度延迟——尤其当新 G 刚被唤醒(比如 netpoll 返回、syscall 完成)或刚入队时,M 休眠再被唤醒的开销远大于短暂空转几轮。
的自旋不是无意义忙等,而是带条件的“等待窗口”:它只在当前 M 处于
状态、且还有其他 P 在活跃时才尝试多次轮询;一旦发现所有 P 都 idle 或自己已非 spinning,就立即转入休眠。
自旋次数和退出条件怎么控制
Go 运行时不设固定自旋次数,而是依赖两个动态信号:
必须为 true —— 这个标志由
或
设置,表示该 M 正在主动找活干,不是被唤醒后被动执行
—— 即至少有一个其他 P 不是 idle 状态,说明系统仍有活跃任务可能产生新 G
只要这两个条件同时满足,
就会循环重试本地队列、全局队列、steal、netpoll;任一条件失效,就跳转到
标签,调用
进入休眠。
什么时候会跳过自旋直接休眠
以下情况会绕过自旋逻辑,快速进入休眠:
当前 M 没有设置
(例如:刚从 syscall 返回但未被 handoffp 激活)
,此时不存在其他 P 可 steal,也无负载均衡必要
所有其他 P 都处于
状态(
),说明系统确实无事可做
netpoll 轮询返回空,且没有定时器待触发(
)
注意:
中的
不是错误跳转,而是正常调度路径终点——它最终调用
把 M 挂起,并释放绑定的 P,让其他 M 可以接手。
容易忽略的细节:spinning 状态不是常驻的
是一个瞬态标记,仅在 M 主动寻找新 G 的几十纳秒到几微秒内有效。它不会跨调度周期保留,也不会在 syscall 返回后自动恢复。这意味着:
一个 M 从阻塞中恢复(如 read 完成),若没被
或
显式设为 spinning,它就会直奔休眠
steal 失败后不重试,是因为 spinning 已被清除,下次进
就不再走自旋分支
高并发场景下,如果大量 M 同时 spinning,运行时会通过
主动降级,避免 CPU 白耗
真正决定“要不要等一会儿”的,从来不是代码写了 loop,而是
这个开关是否被恰当地打开和关闭。
findrunnablespinning_g_.m.spinninghandoffpstartmatomic.Load(&sched.npidle) != gomaxprocs-1findrunnablestopstopmspinninggomaxprocs == 1Pidleatomic.Load(&sched.npidle) == gomaxprocs-1timersWait() == 0findrunnablegoto stoppark_m_g_.m.spinninghandoffpstartmfindrunnableatomic.Xadd(&sched.nmspinning, -1)spinning