G (goroutine)
G: 受管理的轻量线程,使用 go 关键词创建
举例来说, func main() { go other() }, 这段代码创建了两个goroutine,
一个是main, 另一个是other, 注意main本身也是一个goroutine.
- goroutine 的创建,休眠,恢复,停止都受到 go 运行时的管理
- G 执行异步操作时,会进入休眠状态,待操作完成后再恢复,无需占用系统线程
- G 新建或恢复时会添加到运行队列,等待 M 取出运行
M (machine)
M: 在 golang 中等同于系统线程
M 可以运行两种代码:
- go 原生代码,即 goroutine,M 运行 go 需要一个 P
- 原生代码,例如阻塞的 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 核心并无任何关联
- p 可以理解为控制 go 代码的并行度的机制
- 如果 p 的数量等于 1, 代表当前最多只有一个线程 M 执行 go 代码
- 如果等于 2 ,代表最多两个 M 执行 go 代码
- 执行原生代码的线程数量不受 p 控制
因为同一时间只有一个线程 M 可以拥有 P ,P 中的数据都是锁自由的 (lock free)的,读写这些数据的效率会非常高
GMP 模型
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() 调度流程
调度器的生命周期
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会变为此状态