GMP模型
- 1 概念介绍
- 2 调度器策略
- 2.1 线程复用
- work stealing机制
- hand off机制
- 2.2 控制并行
- 2.3 抢占原则
- 3 调度器生命周期
1 概念介绍
golang协程实现的核心机制是GMP。
G代表goroutine协程,M代表内核态线程,P代表处理器(不等同于CPU核心)。
整个GMP模型从下往上可以按照程序空间分为两部分:运行在内核态的部分、运行在用户态的调度器。
运行在内核态的部分从下往上分别为底层硬件CPU核心、OS级别调度器、多个内核线程M。M是真正运行G的实体,OS调度器负责把内核线程分配到底层硬件的CPU核心上去实际执行。
调度器的功能是把可运行的G分配到可以工作的M上。调度器的组成包括P、P维护的本地G队列、全局G队列。P包含了运行G的资源,如果M想运行G,则必须先获取P。全局G队列维护了当前等待运行的G,P的本地G队列与全局G队列作用类似,不过其最多存256个G,如果新建G时发现队列满,则将处于已满状态的P本地队列中的前一半G打乱顺序移动到全局队列。在程序启动时一次性创建所有的P(共GOMAXPROCS
个),存放在数组中。
M和P的数量没有绝对的关系,因为如果一个M发生了阻塞,那么P就会去尝试寻找空闲的M,如果没有空闲的,则会创建新的M。
2 调度器策略
2.1 线程复用
避免频繁的创建、销毁线程,而是尽可能复用线程。
work stealing机制
M在获取P、运行G时,首先尝试从P的本地队列获取G,若队列为空,则M也会先去尝试从其他P的本地队列偷一半放到自己P的本地队列。
hand off机制
当M因为G进行系统调用阻塞时,M会释放绑定的P,把P转移给其他空闲的M执行。
2.2 控制并行
可以基于GOMAXPROCS
来控制并行程度,比如设置GOMAXPROCS=CPU核心数/2
,则最多有一半的CPU核心会并行运行线程。
2.3 抢占原则
一个G一次最多占用CPU10ms时间片,10ms后要重新参与调度,以防止出现G的饥饿现象。这里的抢占式调度实现原理如下。
golang在程序启动的时候,会专门创建一个线程sysmon,用来监控和管理,在内部是一个循环:
1.记录所有P的G任务计数schedtick,schedtick会在每执行一个G任务后递增;
2.如果检查到schedtick一直没有递增,说明这个P一直在执行同一个G任务,如果超过10ms,就在这个G任务的栈信息里面加一个标记;
3.然后这个G任务在执行的时候,如果遇到非内联函数调用,就会检查一次这个标记,然后中断自己,把自己加到队列末尾,执行下一个G;
4.如果没有遇到非内联函数(有时候正常的小函数会被优化成内联函数)调用的话,会一直执行这个G,直到它自己结束,此时如果是个死循环,并且GOMAXPROCS=1的话,程序就永远不会自己停止,且CPU会一直被占用。
3 调度器生命周期
M0是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量runtime.m0中,不需要在堆上分配,M0负责执行初始化操作和启动第一个G,在之后M0就和其他的M一样了。
G0是每次启动一个M都会第一个创建的gourtine,G0仅负责调度的G,其不指向任何可执行的函数,每个M都会有一个自己的G0。在调度或系统调用时会使用G0的栈空间,全局变量中的G0是 M0的G0。