知识卡片

GMP调度路径全景

结构图卡 · 1481.d

内容

一次完整的调度决策把 [[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。