3. 调度循环详解:核心函数调用链

Go 运行时调度器(scheduler)的核心是一个永不停止的循环,它驱动着所有 goroutine 的调度与执行。本章深入剖析调度循环的四个关键函数:schedule()、findRunnable()、execute() 和 goexit(),揭示它们如何协同工作,构成完整的调度契约。

3.1 调度循环总览

每个工作线程(M)在启动后都会进入调度循环。循环的入口是 schedule(),它调用 findRunnable() 获取可运行的 goroutine,然后通过 execute() 执行该 goroutine。当 goroutine 退出或阻塞时,控制权回到 schedule(),形成闭环。

核心契约: 调度循环保证每个 M 在任意时刻要么在执行 goroutine,要么在寻找可运行的 goroutine,永远不会空闲等待(除非所有 P 都空闲)。
调度循环核心流程 调度循环核心流程 schedule() findRunnable() execute() gogo() goexit() 调度循环要点 • schedule() 是循环入口 • findRunnable() 阻塞等待 • execute() 切换上下文 • goexit() 清理并返回

3.2 schedule() — 调度循环入口

schedule() 是每个 M 进入调度循环的起点。它负责调用 findRunnable() 获取下一个可执行的 goroutine,然后调用 execute() 执行它。如果 findRunnable() 返回 nil,表示所有 P 都空闲,M 会进入休眠。

步骤 操作 说明
1 检查 GC 标记 如果当前 P 需要执行 GC 标记工作,优先处理
2 调用 findRunnable() 从本地/全局队列、网络轮询器、其他 P 获取 goroutine
3 检查 gp 是否为 nil 如果为 nil,M 进入休眠等待唤醒
4 调用 execute(gp) 将 goroutine 绑定到当前 M 并执行
关键观察: schedule() 本身不包含复杂的查找逻辑,它将查找职责委托给 findRunnable(),体现了单一职责原则。

3.3 findRunnable() — 多级查找策略

findRunnable() 是调度循环中最复杂的函数,它实现了多级查找策略,确保 goroutine 能够被高效地分配到空闲的 M 上。查找顺序如下:

  1. 本地运行队列(LRQ): 从当前 P 的本地队列中获取 goroutine,复杂度 O(1)。
  2. 全局运行队列(GRQ): 从全局队列中获取,每 61 次调度检查一次,防止全局队列饥饿。
  3. 网络轮询器(netpoll): 检查是否有因 I/O 事件就绪的 goroutine。
  4. 任务窃取(work stealing): 从其他 P 的本地队列中窃取一半 goroutine。
  5. 空闲等待: 如果所有 P 都空闲,M 进入休眠,等待唤醒信号。
优先级 查找目标 复杂度 触发条件
1 本地运行队列 O(1) 每次调度
2 全局运行队列 O(1) 每 61 次调度
3 网络轮询器 O(n) 本地/全局队列为空
4 任务窃取 O(n) 以上均无结果
5 空闲等待 — 所有 P 空闲
性能陷阱: 任务窃取虽然能平衡负载,但频繁窃取会导致缓存亲和性下降。Go 调度器通过随机选择目标 P 和窃取一半 goroutine 来平衡负载与缓存效率。
findRunnable() 多级查找策略 findRunnable() 多级查找策略 findRunnable() 1. 本地运行队列 2. 全局运行队列 3. 网络轮询器 4. 任务窃取 5. 空闲等待 找到 gp 查找策略特点 • 优先级从高到低 • 每级失败后降级 • 空闲等待会阻塞 M

3.4 execute() — 上下文切换

execute() 负责将选中的 goroutine 绑定到当前 M 上,并执行上下文切换。它执行以下关键操作:

  • 绑定 goroutine: 将 goroutine 的 m 字段指向当前 M,将 M 的 curg 字段指向该 goroutine。
  • 更新状态: 将 goroutine 状态从 _Grunnable 切换为 _Grunning。
  • 调用 gogo(): 通过汇编函数 gogo() 恢复 goroutine 的栈和寄存器上下文,开始执行用户代码。
// 简化后的 execute() 逻辑
func execute(gp *g) {
    // 1. 绑定 goroutine 到当前 M
    gp.m = getg().m
    getg().m.curg = gp

    // 2. 更新状态
    casgstatus(gp, _Grunnable, _Grunning)

    // 3. 执行上下文切换
    gogo(&gp.sched)
}
关键点: gogo() 是汇编实现的函数,它直接操作栈指针和程序计数器,完成从调度器栈到 goroutine 栈的切换。一旦 gogo() 返回,控制权就完全交给了 goroutine 的用户代码。

3.5 goexit() — 优雅退出与清理

当 goroutine 执行完毕(从 go func() 返回)或调用 runtime.Goexit() 时,控制权会进入 goexit() 函数。它负责:

  1. 执行 defer 函数: 确保所有注册的 defer 函数按 LIFO 顺序执行。
  2. 清理资源: 释放 goroutine 占用的栈空间和其他资源。
  3. 更新状态: 将 goroutine 状态切换为 _Gdead。
  4. 返回调度循环: 调用 schedule() 重新进入调度循环,寻找下一个可执行的 goroutine。
阶段 操作 说明
defer 执行 调用 gopanic()/gorecover() 处理 panic 和 recover,确保 defer 链完整执行
栈清理 调用 stackfree() 释放 goroutine 的栈内存
状态切换 casgstatus(gp, _Grunning, _Gdead) 标记 goroutine 为死亡状态
循环返回 调用 schedule() 重新进入调度循环
设计意图: goexit() 的设计确保了 goroutine 的退出不会导致 M 空闲。M 在清理完当前 goroutine 后立即返回调度循环,继续执行其他 goroutine,实现了 M 的最大利用率。

3.6 完整调用链总结

调度循环的完整调用链可以概括为:

schedule() → findRunnable() → execute() → gogo() → [用户代码] → goexit() → schedule()

这个循环在 Go 运行时中永不停止。每个 M 都在这个循环中运行,直到程序退出。理解这个调用链是掌握 Go 调度器行为的关键。

函数 职责 调用者 被调用者
schedule() 调度循环入口,协调查找与执行 goexit()、mstart() findRunnable()、execute()
findRunnable() 多级查找可运行 goroutine schedule() runqget()、globrunqget()、netpoll()、runqsteal()
execute() 绑定 goroutine 并切换上下文 schedule() gogo()
goexit() 清理 goroutine 并返回调度循环 用户代码返回 schedule()
契约总结: 调度循环的核心契约是“永不空闲”。通过 findRunnable() 的多级查找策略和 goexit() 的自动回环机制,Go 调度器保证了每个 M 都能持续执行 goroutine,实现了高效的并发调度。