Golang GMP 调度模型

什么是 GMP

GMP 是 goroutine 调度器的调度模型。

  • G:goroutine 协程;
  • M:thread 线程;
  • P:processor 处理器/调度器。

M 是运行 G 的实体,P 的功能是把可运行的 G 分配到 M 上。

在 GMP 模型中,G 被放在队列中等待被调度,而队列又分为两种:全局队列和本地队列,正是因为本地队列的加入才有了 GMP 模型,否则只是 GM 模型,所以每个 P 都有自己的本地队列。这样做的好处是,避免了所有的处理器都需要去全局队列中获取 G,产生竞争降低性能。

在创建 G 的时候,首先将 G 放入本地队列中,如果本地队列已经满了,则将本地队列中的一半移动到全局队列中。

如果线程想要运行任务,则要先获取到 P,在 P 中的本地队列中获取 G,当本地队列为空时,M 就会尝试去全局队列中拿一批 G 放到本地队列,或者也可以从其它 P 的本地队列中偷取一半到自己获取的 P 的本地队列。

有关 M 和 P 的个数问题

  1. P 的数量:P 的数量由环境变量 $GOMAXPROCS 来决定,也可以通过 runtim.GOMAXPROCS() 来设置。

  2. M 的数量:go 程序启动时,默认设置的线程数量为 10000。当一个 M 阻塞时,会创建新的 M。

P 和 M 的数量并没有绝对的关系,当一个 M 阻塞了,P 就会去创建或切换到另一个 M。所以即使只有 1 个 P 也可能会有很多个 M。

当 P 的最大数量已经决定后,runtime 就会根据这个数量去创建 P。而 M 是在没有足够的 M 关联 P 并运行 P 中的 G 时会创建新的 M。

调度器的设计策略

核心思想:复用线程,避免频繁创建、销毁线程的开销,尽量的复用线程。

  1. work stealing 机制:当 M 关联的 P 上没有可以运行的 G 时,去其它 P 那里窃取 G 来继续运行,而不会销毁线程 G;
  2. hand off 机制:当 M 进行系统调用陷入内核前,该 M 会让出 P,解除于当前 P 的强关联。让其它 M 使用,但会在 M 中记录这个让出的 P,当该 M 从系统调用中被恢复时,会去找让出来的这个 P,如果没被占用就继续使用,否则将自己 G 放到全局队列中去,进入休眠。

并且,在 Golang 中,协程实现了抢占式调度,所以一个 goroutine 最多能占用 CPU 10 ms,防止其它 goroutine 被饿死。

在 go1.13 之前的抢占式调度都依赖于栈增长检测代码,但如果不进行函数调用,就没有机会触发栈增长检测,如陷入了一个死循环,这样就无法让出 CPU 了,所以在 go1.13 之前的抢占式调度都不算真正意义上的抢占式调度。

在 go1.14 中实现了真正的异步抢占。在 Unix 系统中,会向需要让出的线程发起 SIGPREEMPT 信号,收到该信号的线程会进入中断,去执行相应的信号处理函数,检测到信号为 SIGPREEMPT 时,会向当前线程的上下文中注入一个异步抢占函数,在线程从内核返回时,会去执行异步抢占函数。

M0 和 G0

M0 是程序启动之后编号为 0 的主线程,这个 M 对应的实例在 runtime.m0 中,它负责初始化操作和启动第一个 G,在这之后 M0 就和其他 M 没区别了。

G0 则是每次启动一个 M 都会创建的第一个 goroutine,G0 仅仅用于调度和协调其它 goroutine,而不负责运行函数。在系统调用或调度时会使用 G0 的栈空间。

GMP 中有哪些状态?

  • G 的状态:
    1. _Gidle:刚刚分配还没有被初始化;
    2. _Grunnable:处在就绪态的 goroutine,没有执行代码,还在本地队列或全局队列中等待被调度;
    3. _Grunning:正在执行代码的 goroutine;
    4. _Gsyscall:正在执行系统调用,与某个 M 绑定,但和该 M 一起与 P 脱离;
    5. _Gwaiting:被阻塞的 goroutine,可能阻塞在某个 chan 上;
    6. _Gdead:表示当前 goroutine 已经结束执行或已被垃圾回收,不再具有实际的执行意义。
    7. _Gcopystack:栈正在被复制。
    8. _Gscan:GC 正在扫描栈空间,没有执行代码,可以和其他状态同时存在。
  • P 的状态:
    1. _Pidle:当前 P 没有运行用户代码,且运行队列为空;
    2. _Prunning:当前 P 被 M 持有,并且正在运行用户代码;
    3. _Psyscall:当前 P 没有执行用户代码,并且绑定的 M 陷入系统调用;
    4. _Pgcstop:当前 P 由于 GC 而进入 STW;
    5. _Pdead:当前 P 已经不再使用。
  • M 的状态:
    1. 自旋线程:处于运行状态但没有可执行的 goroutine 的线程,数量最多为 GOMAXPROC 个,大于这个数量的 M 会进入休眠;
    2. 非自旋线程:处于运行状态并且有可执行 goroutine 的线程。

G 状态的转换

![G 状态转变图](../images/G 状态转变图.png)