GMP
含义
Goroutine的并发编程模型基于GMP模型,简要解释一下GMP的含义:
G:表示goroutine,每个goroutine都有自己的栈空间,定时器,初始化的栈空间在2k左右,空间会随着需求增长。
M:抽象化代表内核工作线程,记录内核线程栈信息,当goroutine调度到线程时,使用该goroutine自己的栈信息。
P:代表调度器,负责调度goroutine到M,维护一个本地goroutine队列,M从P上获得goroutine并执行,同时还负责部分内存的管理。
模型
从大体看一下GMP模型。
M代表一个工作线程,在M上有一个P和G绑定,P是绑定到M上的,G是通过P的调度获取的,在某一时刻,一个M上只有一个G(g0除外)。在P上拥有一个G队列,里面是已经就绪的G,是可以被调度到线程栈上执行的协程,称为运行队列。
接下来看一下程序中GMP的分布。
每个进程都有一个全局的G队列,也拥有P的本地执行队列,同时也有不在运行队列中的G。如正处于channel的阻塞状态的G,还有脱离P绑定在M的(系统调用)G,还有执行结束后进入P的gFree列表中的G等等,接下来列举一下常见的几种状态。
状态汇总
G状态
G的主要几种状态:
本文基于Go1.13,具体代码见(<GOROOT>/src/runtime/runtime2.go)
_Gidle:刚刚被分配并且还没有被初始化,值为0,为创建goroutine后的默认值
_Grunnable: 没有执行代码,没有栈的所有权,存储在运行队列中,可能在某个P的本地队列或全局队列中(如上图)。
_Grunning: 正在执行代码的goroutine,拥有栈的所有权(如上图)。
_Gsyscall:正在执行系统调用,拥有栈的所有权,与P脱离,但是与某个M绑定,会在调用结束后被分配到运行队列(如上图)。
_Gwaiting:被阻塞的goroutine,阻塞在某个channel的发送或者接收队列(如上图)。
_Gdead: 当前goroutine未被使用,没有执行代码,可能有分配的栈,分布在空闲列表gFree,可能是一个刚刚初始化的goroutine,也可能是执行了goexit退出的goroutine(如上图)。
_Gcopystac:栈正在被拷贝,没有执行代码,不在运行队列上,执行权在
_Gscan : GC 正在扫描栈空间,没有执行代码,可以与其他状态同时存在
P的状态
_Pidle :处理器没有运行用户代码或者调度器,被空闲队列或者改变其状态的结构持有,运行队列为空
_Prunning :被线程 M 持有,并且正在执行用户代码或者调度器(如上图)
_Psyscall:没有执行用户代码,当前线程陷入系统调用(如上图)
_Pgcstop :被线程 M 持有,当前处理器由于垃圾回收被停止
_Pdead :当前处理器已经不被使用
M的状态
自旋线程(休眠状态):处于运行状态但是没有可执行G的线程,数量最多为GOMAXPROC,若是数量大于GOMAXPROC就会进入休眠。
非自旋线程(非休眠状态):处于运行状态有可执行goroutine的线程。
调度场景
Channel阻塞:当goroutine读写channel发生阻塞时候,会调用gopark函数,该G会脱离当前的M与P,调度器会执行schedule函数调度新的G到当前M。可参考上一篇文章channel探秘。
系统调用:当某个G由于系统调用陷入内核态时,该P就会脱离当前的M,此时P会更新自己的状态为Psyscall,M与G互相绑定,进行系统调用。结束以后若该P状态还是Psyscall,则直接关联该M和G,否则使用闲置的处理器处理该G。
系统监控:当某个G在P上运行的时间超过10ms时候,或者P处于Psyscall状态过长等情况就会调用retake函数,触发新的调度。
主动让出:由于是协作式调度,该G会主动让出当前的P,更新状态为Grunnable,该P会调度队列中的G运行。
总结
runtime 准备好 G, M, P, 然后 M 绑定 P, M 从各种队列中获取 G, 切换到 G 的执行栈上并执行 G 上的任务函数,调用 goexit 做清理工作并回到 M, 如此反复。