GMP模式

什么是协程#

对于进程、线程,都是有内核进行调度,有 CPU 时间片的概念,进行抢占式调度。协程,又称微线程,纤程。英文名 Coroutine。协程的调用有点类似子程序,如程序 A 调用了子程序 B,子程序 B 调用了子程序 C,当子程序 C 结束了返回子程序 B 继续执行之后的逻辑,当子程序 B 运行结束了返回程序 A,直到程序 A 运行结束。但是和子程序相比,协程有挂起的概念,协程可以挂起跳转执行其他协程,合适的时机再跳转回来。

线程调度原理#

N:1 模型,多个用户空间线程在 1 个内核空间线程上运行。优势是上下文切换非常快,因为这些线程都在内核态运行,但是无法利用多核系统的优点。

1:1 模型,1 个内核空间线程运行一个用户空间线程。这种充分利用了多核系统的优势但是上下文切换非常慢,因为每一次调度都会在用户态和内核态之间切换。POSIX 线程模型 (pthread) 就是这么做的。

M:N 模型,内核空间开启多个内核线程,一个内核空间线程对应多个用户空间线程。效率非常高,但是管理复杂。

goroutine 调度原理#

相比其他语言,golang 采用了 MPG 模型管理协程,更加高效,但是管理非常复杂。#
  • M:内核级线程
  • G:代表一个 goroutine
  • P:Processor,处理器,用来管理和执行 goroutine 的。
G-M-P 三者的关系与特点:#

P 的个数取决于设置的 GOMAXPROCS,go 新版本默认使用最大内核数,比如你有 8 核处理器,那么 P 的数量就是 8

M 的数量和 P 不一定匹配,可以设置很多 M,M 和 P 绑定后才可运行,多余的 M 处于休眠状态。
P 包含一个 LRQ(Local Run Queue)本地运行队列,这里面保存着 P 需要执行的协程 G 的队列
除了每个 P 自身保存的 G 的队列外,调度器还拥有一个全局的 G 队列 GRQ(Global Run Queue),这个队列存储的是所有未分配的协程 G。

假设我们的主机是单核的,那么协程运行图是这样:#

红色部分表示挂起和休眠,黄色部分表示准备就绪等待运行,绿色部分表示正在运行。

主机是单核的所以只有一个处理器 P,但是系统初始化了两个线程 M0 和 M1,处理器 P 优先绑定了 M0 线程,M1 进入休眠状态。
P 的 LRQ 队列里有 G1,G2,G3 等待处理。P 目前正在处理 G0, 全局等待队列 GRQ 里保存着 G4,G5,表示这两个协程还未分配给 P。
如果 G0 在短时间内处理完,P 就会从 LRQ 中取出 G1 继续处理。并且将 GRQ 全局队列中的部分协程加入 LRQ 中。
如下图

假设现在 G1 处理速度很慢,系统就会让 M0 线程休眠,挂起协程 G1,唤醒线程 M1 进行处理其他的协程。这里 M1 会将 M0 未处理的协程取走处理。

等到 M1 协程队列中所有协程处理完再次唤醒 M0,或者 M1 处理某个协程时间较长被挂起,M0 也会被唤醒。

上面的讨论是单核主机情况,如果是多核的,就会运行多个 P 和 M,如下图

M0 和 M1 分别运行在不同的内核线程中,M0 处理 G1,G2,G3,M1 处理 G4,G5,G6。
有人会问,当 M0 处理完所有的协程,而 M1 还未处理完,系统会如何做呢?
M0 会取走 M1 的一半数量未处理的协程。

总结#

本作品采用《CC 协议》,转载必须注明作者和本文链接
good good study day day up