GMP 调度器

62 阅读3分钟

· GMP 调度器是 Go 语言处理并发的关键组件。G 是轻量级用户线程,M 是操作系统线程,P 为逻辑处理器,数量常与 CPU 核心数一致。初始化时创建 P、设本地队列,将主协程放入全局队列。调度循环中,M 先绑定 P,从 P 的本地队列(优先)、全局队列或 “偷取” 其他 P 队列获取 G 执行。G 遇 I/O 等阻塞时,M 与 P 解绑,完成后 G 重新入队。调度有协作式和强制抢占两种。通过 M:N 模型、本地队列策略、灵活处理系统调用和工作窃取算法,实现高效并发管理 。

G(Goroutine)

· 就是咱写 Go 代码时弄的那些轻量级线程,一个 G 就能跑一个函数

· 它带着函数运行要用的调用栈,还有当前的状态信息啥的

· 状态有准备跑(runnable)、正在跑(running)、被堵住(blocked)这些情况

· M(Machine)

· 对应到操作系统那边的线程,实实在在在 CPU 上干活的

· 要让 Goroutine 真正跑起来,M 得找个 P “搭档” 才行

· P(Processor)

· 是个逻辑上的处理器,它管着 Goroutine 的一个本地小队列(runq)

· 它还记着一些调度要用的信息,像 PCounter 能帮着决定啥时候该抢着调度了

· 有多少个 P 是由 GOMAXPROCS 这个参数定的,一般默认和咱电脑 CPU 核心数一样

工作流程

 

· 初始化阶段

· 程序一开始,就按 GOMAXPROCS 的值创建一堆 P 出来

· 每个 P 都给自己弄个本地的运行队列 runq

· 程序最开始的那个主协程 main,就被放到全局队列 global runq 里等着

· 调度循环(SchedLoop)

· M 得先找到一个 P,然后就进入 SchedLoop 这个循环开始干活

· 找要运行的 Goroutine 时,先看 P 的本地队列 runq,而且是从最后放进去的开始找(LIFO,这样能少点锁竞争)

· 要是本地队列空了,就去全局队列 global runq 里找,不过去全局队列找得加锁,麻烦点

· 要是全局队列也空了,就从别的 P 的本地队列偷一半 Goroutine 过来(这叫工作窃取)

· Goroutine 执行

· M 就开始执行 G 的函数

· 要是 G 碰到 I/O 操作或者系统调用,就被标记成 blocked,从 P 的队列里拿走

· 这时候 M 就和 P 分开,M 可以去睡觉或者干点别的事

· 等 I/O 操作完了,G 又能跑了(runnable),就放回原来 P 的队列,或者全局队列里

· 抢占式调度

· 协作式抢占:写代码的时候,要是咱调用 runtime.Gosched (),Goroutine 就主动把 CPU 让出来

· 强制抢占:一种是利用信号,函数调用的时候会检查 stackPreempt 标志看要不要抢占;还有就是系统调用完回来的时候,也会看看要不要抢占

关键机制

 

· M:N 模型

· 就是说 M 个操作系统线程,能对应跑 N 个 Goroutine,靠 P 这么一调度,效率就高起来了

· 减少锁竞争

· 本地队列 runq 用 LIFO 这种取法,能少去动锁,全局队列只有没办法了才去访问,也得加锁,尽量少折腾

· 系统调用处理

· 要是 Goroutine 遇到阻塞式系统调用,M 和 P 就分开,别的 M 就能用这个 P 接着干活了

· 工作窃取算法

· 这么干能让各个 P 的工作量差不多,不会有的 P 累死,有的 P 闲着,资源浪费