Golang 的调度器可以描述为 GPM 模型,GPM 的含义如下:

  • G 代表 groutine
  • P 代表 logical processor,逻辑处理器,每个 G 需要运行都需要分配到一个 P
  • M 代表 machine 是一个 OS Thread 的抽象,只有绑定了 M 的 P 才能真正的运行

GPM 模型的样子可以抽象为下面这张图:

go-gpm-scheduler.png

抢占式调度

在 go1.14 之前,Golang 的调度器是基于协作的抢占式调度,其工作原理大概如下:

  1. 编译器会在调用函数前插入 runtime.morestack
  2. Go 语言运行时会在垃圾回收暂停程序、系统监控发现 Goroutine 运行超过 10ms 时发出抢占请求 StackPreempt
  3. 当发生函数调度时,可能会执行编译器插入的 runtime.morestack,它调用 runtime.newstack 会检查 Goroutine 的 stackguard0 字段是否为 StackPreempt;
  4. 如果 stackguard0StackPreempt,就会触发抢占让出当前线程

在 go1.14 之后,Golang 实现了基于信号(SIGURG)的抢占式调度,其调度过程大概如下:

  1. 程序启动时,在 runtime.sighandler 中注册 SIGURG 信号的处理函数 runtime.doSigPreempt;
  2. 在触发垃圾回收的栈扫描时会调用 runtime.suspendG 挂起 Goroutine,该函数执行两个功能:
    1. _Grunning 状态的 Goroutine 标记称可以被抢占,preemptStop 设置为 true
    2. 调用 runtime.preemptM 触发抢占;
  3. runtime.preemptM 会调用 runtime.signalM 向线程发送程序 SIGURG
  4. 操作系统会中断正在运行的线程并执行预先注册的信号处理函数,(1) 中注册的 runtime.doSigPreempt
  5. runtime.doSigPreempt 函数会处理抢占信号,获取当前的 SP 和 PC 寄存器并调用 runtime.sigctxt.pushCall
  6. runtime.sigctxt.pushCall 会修改寄存器并在程序回到用户态时执行 runtime.asyncPreempt
  7. 汇编 runtime.asyncPreempt 会调用运行时函数 runtime.asyncPreempt2
  8. runtime.asyncPreempt2 会调用 runtime.preemptPark;
  9. runtime.preemptPark 会修改当前 Goroutine 的状态到 _Gpreempted 并调用 runtime.schedule 让当前函数陷入休眠并让出线程,调度器会选择其它的 Goroutine 继续执行。

Reference

  • Go 语言设计与实现
  • 《Go 语言精进之路:从新手到高手的编程思想、方法和技巧 1》,32.2 goroutine 调度模型与演进过程。