知识卡片
GMP调度路径全景
内容
一次完整的调度决策把 [[G0负责调度]]、[[队列兼顾公平]]、[[窃取平衡负载]]、[[系统调用要交接]] 串成先后顺序:M 上的用户协程让出后先切到 g0,schedule 函数按 [[队列兼顾公平]] 所述”本地 runnext → 本地队列 → 每 61 次强制看一次全局队列 → 就绪的网络协程 → 从其他 P 窃取一半”的顺序查找可运行的 G;若中途协程进入系统调用,运行时会把 P 与阻塞的 M 解绑并交给空闲 M 接管,系统调用结束后再尝试重新绑定原 P 或申请新 P。这条路径解释了为什么”局部优先、定期公平、找不到就偷”能在不中心化加锁的前提下同时兼顾缓存局部性和负载均衡。
结构图:
flowchart TD
A[M 上的 G 让出/被抢占] --> B[切换到 g0]
B --> C[schedule 函数开始调度]
C --> D{P.runnext 非空?}
D -->|是| Z[执行该 G]
D -->|否| E{本地队列有 G?}
E -->|是| Z
E -->|否| F{已执行满 61 次调度?}
F -->|是| G[优先查全局队列]
F -->|否| H{全局队列有 G?}
G --> Z
H -->|是| Z
H -->|否| I{就绪的网络协程?}
I -->|是| Z
I -->|否| J[随机挑选其他 P 窃取一半 G]
J --> Z
Z --> K[gogo 切换执行用户 G]
K --> L{G 进入系统调用?}
L -->|是| M[P 与阻塞 M 解绑, 交给空闲 M]
M --> N[系统调用结束, exitsyscall 重新绑定 P]
N --> C
L -->|否| C
参考来源
- 位置:《Go语言底层原理剖析》第15章《深入协程设计与调度原理》15.4节《调度循环》、15.5节《调度策略》与15.6.4节末尾系统调用抢占部分
- 结论依据:原文在15.4节说明调度循环经历"schedule函数到execute函数再到gogo函数的过程";15.5节明确调度器查找顺序为"先尝试从runnext中获取下一个执行的G……如果局部运行队列为空,则尝试从全局运行队列中获取……如果窃取不到任务,那么当前的P会解除与M的绑定",并单独说明"P中每执行61次调度,就需要优先从全局队列中获取一个G";15.6.4节末尾说明系统调用时P与M解绑、由其他M接管,返回后exitsyscall尝试重新绑定P。四段合并即是本卡片结构图的直接依据,图中每个节点、分支和跳转都能在原文对应小节找到出处。
- 原始内容:调度协程的优先级与顺序如图15-6所示。排除从全局队列中获取这种情况,每个P在执行调度时,都会先尝试从runnext中获取下一个执行的G,如果runnext为空,则继续从当前P中的局部运行队列runq中获取需要执行的G;如果局部运行队列为空,则尝试从全局运行队列中获取需要执行的G;如果全局队列也没有找到要执行的G,则会尝试从其他的P中窃取可用的协程……P中每执行61次调度,就需要优先从全局队列中获取一个G到当前P中,并执行下一个要执行的G。