跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

Go 调度器中 findrunnable 函数的自旋与空闲处理逻辑

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

相关文章