Golang 的调度器可以描述为 GPM 模型,GPM 的含义如下:
- G 代表 groutine
- P 代表 logical processor,逻辑处理器,每个 G 需要运行都需要分配到一个 P
- M 代表 machine 是一个 OS Thread 的抽象,只有绑定了 M 的 P 才能真正的运行
GPM 模型的样子可以抽象为下面这张图:
抢占式调度
在 go1.14 之前,Golang 的调度器是基于协作的抢占式调度,其工作原理大概如下:
- 编译器会在调用函数前插入
runtime.morestack
- Go 语言运行时会在垃圾回收暂停程序、系统监控发现 Goroutine 运行超过 10ms 时发出抢占请求
StackPreempt
- 当发生函数调度时,可能会执行编译器插入的
runtime.morestack
,它调用runtime.newstack
会检查 Goroutine 的stackguard0
字段是否为StackPreempt
; - 如果
stackguard0
是StackPreempt
,就会触发抢占让出当前线程
在 go1.14 之后,Golang 实现了基于信号(SIGURG
)的抢占式调度,其调度过程大概如下:
- 程序启动时,在
runtime.sighandler
中注册SIGURG
信号的处理函数runtime.doSigPreempt
; - 在触发垃圾回收的栈扫描时会调用
runtime.suspendG
挂起 Goroutine,该函数执行两个功能:- 将
_Grunning
状态的 Goroutine 标记称可以被抢占,preemptStop
设置为true
; - 调用
runtime.preemptM
触发抢占;
- 将
runtime.preemptM
会调用runtime.signalM
向线程发送程序SIGURG
;- 操作系统会中断正在运行的线程并执行预先注册的信号处理函数,(1) 中注册的
runtime.doSigPreempt
; runtime.doSigPreempt
函数会处理抢占信号,获取当前的 SP 和 PC 寄存器并调用runtime.sigctxt.pushCall
;runtime.sigctxt.pushCall
会修改寄存器并在程序回到用户态时执行runtime.asyncPreempt
;- 汇编
runtime.asyncPreempt
会调用运行时函数runtime.asyncPreempt2
; runtime.asyncPreempt2
会调用runtime.preemptPark
;runtime.preemptPark
会修改当前 Goroutine 的状态到_Gpreempted
并调用runtime.schedule
让当前函数陷入休眠并让出线程,调度器会选择其它的 Goroutine 继续执行。
Reference
- Go 语言设计与实现
- 《Go 语言精进之路:从新手到高手的编程思想、方法和技巧 1》,32.2 goroutine 调度模型与演进过程。