Golang——GMP模型

263 阅读5分钟

G (goroutine)

G: 受管理的轻量线程,使用 go 关键词创建

举例来说, func main() { go other() }, 这段代码创建了两个goroutine,
一个是main, 另一个是other, 注意main本身也是一个goroutine.

  1. goroutine 的创建,休眠,恢复,停止都受到 go 运行时的管理
  2. G 执行异步操作时,会进入休眠状态,待操作完成后再恢复,无需占用系统线程
  3. G 新建或恢复时会添加到运行队列,等待 M 取出运行

M (machine)

M: 在 golang 中等同于系统线程

M 可以运行两种代码:

  1. go 原生代码,即 goroutine,M 运行 go 需要一个 P
  2. 原生代码,例如阻塞的 syscall ,M 运行原生代码不需要 P

M会从运行队列中取出 G, 然后运行 G, 如果 G 运行完毕或者进入休眠状态, 则从运行队列中取出下一个G运行, 周而复始.
有时候 G 需要调用一些无法避免阻塞的原生代码, 这时M会释放持有的P并进入阻塞状态, 其他 M 会取得这个 P 并继续运行队列中的 G.
go需要保证有足够的 M 可以运行 G, 不让 CPU 闲着, 也需要保证 M 的数量不能过多.

P (process)

P: 代表 M 运行 G 所需要的资源
一些讲解协程的文章把 p 理解为 cpu 核心,其实是不正确的
虽然 p 的数量默认等于 cpu 核心数,但可以通过改变环境变量 GOMAXPROC 修改,在实际运行时 p 跟 cpu 核心并无任何关联

  1. p 可以理解为控制 go 代码的并行度的机制
  2. 如果 p 的数量等于 1, 代表当前最多只有一个线程 M 执行 go 代码
  3. 如果等于 2 ,代表最多两个 M 执行 go 代码
  4. 执行原生代码的线程数量不受 p 控制

因为同一时间只有一个线程 M 可以拥有 P ,P 中的数据都是锁自由的 (lock free)的,读写这些数据的效率会非常高

GMP 模型

image.png

M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来。

P和M何时会被创建

1、P何时创建:在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P。

2、M何时创建:没有足够的M来关联P并运行其中的可运行的G。比如所有的M此时都阻塞住了,而P中还有很多就绪任务,就会去寻找空闲的M,而没有空闲的,就会去创建新的M。

调度器的设计策略

go func()  调度流程

image.png

调度器的生命周期

image.png

M0

M0是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量runtime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G, 再之后M0就和其他的M一样了。

G0

G0是每次启动一个M都会第一个创建的gourtine,G0仅用于负责调度的G,G0不指向任何可执行的函数, 每个M都会有一个自己的G0。在调度或系统调用时会使用G0的栈空间, 全局变量的G0是M0的G0。

总结,Go调度器很轻量也很简单,足以撑起goroutine的调度工作,并且让Go具有了原生(强大)并发的能力。Go调度本质是把大量的goroutine分配到少量线程上去执行,并利用多核并行,实现更强大的并发。

数据结构

在讲解协程的工作流程之前, 还需要理解一些内部的数据结构.

G的状态

  • 空闲中(_Gidle): 表示G刚刚新建, 仍未初始化
  • 待运行(_Grunnable): 表示G在运行队列中, 等待M取出并运行
  • 运行中(_Grunning): 表示M正在运行这个G, 这时候M会拥有一个P
  • 系统调用中(_Gsyscall): 表示M正在运行这个G发起的系统调用, 这时候M并不拥有P
  • 等待中(_Gwaiting): 表示G在等待某些条件完成, 这时候G不在运行也不在运行队列中(可能在channel的等待队列中)
  • 已中止(_Gdead): 表示G未被使用, 可能已执行完毕(并在freelist中等待下次复用)
  • 栈复制中(_Gcopystack): 表示G正在获取一个新的栈空间并把原来的内容复制过去(用于防止GC扫描)

M的状态

M并没有像G和P一样的状态标记, 但可以认为一个M有以下的状态:

  • 自旋中(spinning): M正在从运行队列获取G, 这时候M会拥有一个P
  • 执行go代码中: M正在执行go代码, 这时候M会拥有一个P
  • 执行原生代码中: M正在执行原生代码或者阻塞的syscall, 这时M并不拥有P
  • 休眠中: M发现无待运行的G时会进入休眠, 并添加到空闲M链表中, 这时M并不拥有P

自旋中(spinning)这个状态非常重要, 是否需要唤醒或者创建新的M取决于当前自旋中的M的数量.

P的状态

  • 空闲中(_Pidle): 当M发现无待运行的G时会进入休眠, 这时M拥有的P会变为空闲并加到空闲P链表中
  • 运行中(_Prunning): 当M拥有了一个P后, 这个P的状态就会变为运行中, M运行G会使用这个P中的资源
  • 系统调用中(_Psyscall): 当go调用原生代码, 原生代码又反过来调用go代码时, 使用的P会变为此状态
  • GC停止中(_Pgcstop): 当gc停止了整个世界(STW)时, P会变为此状态
  • 已中止(_Pdead): 当P的数量在运行时改变, 且数量减少时多余的P会变为此状态