schedule函数在M需执行G时被调用,典型场景包括M启动、G让出、阻塞恢复及系统调用返回;它永不返回,选中G后即通过execute跳转执行。

schedule 函数在什么时候被调用
它不是用户能直接调用的函数,而是 Go 运行时调度循环的核心入口,每次 M 需要找一个 G 来执行时就会进入 schedule。典型触发场景包括:M 刚启动、G 执行完主动让出(如 runtime.Gosched())、G 阻塞后恢复、系统调用返回后找不到 P 等。
注意:schedule 是一个永不返回的函数——一旦选中 G,就通过 execute 切换上下文并跳转执行,不会“返回”到调用它的栈帧。
schedule 里怎么找可运行的 G
查找顺序有明确优先级,直接影响调度延迟和局部性表现:
-
pp.runnext:高优先级单槽队列,比如刚被唤醒或被标记为 next 的 G,优先级最高 - 本地队列
pp.runq:环形队列,最多 256 个 G;schedule默认先从这里取,保证 cache locality - 全局队列
runtime.globrunq:当本地队列为空时,会批量取(默认一次取 32 个)填入本地队列,避免频繁锁竞争 - 网络轮询器
netpoll:检查是否有已完成 I/O 的 G(比如read就绪),这部分由findrunnable内部调用netpoll(false)完成 - 工作窃取(Work Stealing):当前 P 的队列为空时,随机选其他 P,尝试窃取其
runq中一半的 G
其中有个关键细节:本地队列每执行约 60 个 G 后,会强制从全局队列取一次,防止长期饥饿——这不是严格轮询,而是概率性掺杂,靠 atomic 计数器控制。
schedule 执行前后的状态切换容易出错
真正容易被忽略的是状态同步和资源归属问题:
- M 必须绑定 P 才能调用
schedule;若 M 当前没 P(比如刚从系统调用返回),会先尝试获取空闲 P,失败则触发startm创建新 M -
pp.preempt = false在进入findrunnable前被重置,否则可能干扰抢占逻辑 - G 被选中后,会设置
gp.m = mp、gp.p = pp,并把状态从_Grunnable改为_Grunning;如果此时 G 正在被 GC 扫描或正在被抢占,findrunnable可能跳过它 - 若所有队列都为空且 netpoll 无就绪 G,M 会进入自旋(
spinning = true),持续尝试窃取,直到超时或被唤醒——这阶段不释放 P,但会降低 CPU 占用
最隐蔽的问题是:P 的状态必须与 M 的绑定关系严格一致。比如系统调用退出时若 exitsyscall 没成功抢回原 P,G 就会被丢进全局队列,而不是回到原 LRQ —— 这会导致后续执行延迟升高,尤其在高并发短任务场景下明显。

















