1.简介<br />
协程是G语言的一大亮点,关于协程的调度机制还不甚了解,通过查看源码和搜资源,对Go的GMP调度模型做了整理;针对一些资料图例简单、一些问题讲解不清的问题,重新画了流程图,由于时间有限,本文主要是围绕流程图讲解,后续有空在针对源码部分,逐个详细介绍。<br />
2.GMP<br />
2.1构成<br />
2.1.1元素<br />
就本质而言,协程是用户态下的执行单元,依附于线程上,所以从系统角度看,GMP的模型示意图是:<br />
![image.png](https://static.studygolang.com/200825/7eb3e41f76b46ce743a7a32be2d1580e.png)
<br />
GMP调度模型主要是由G、M、P三元素构成,除此之外,还有全局队列、本地队列等元素;首先看下G、M、P三元素:<br />
1)G Goroutine,对应协程,是用户态下的执行单元,拥有独立的栈空间,可以存放当前的运行内存及状态<br />
2)M Thread,对应内核线程,负责运行G,M的数量是动态变化的,上限是10000<br />
3)P Processor,对应处理器,用于存放G,队列大小是256,P的数量代表最大并行程度,一般与核数相等<br />
而全局G队列、本地G队列用于G的存储,空闲M队列和空闲P队列是用于存放暂时无事可做的M、P。<br />
2.1.2分类<br />
M可以细分为:<br />
1)任务线程 M<br />
主要是用于任务执行,实现G的循环执行<br />
2)第一个线程 M0<br />
主要是完成初始化操作,待初始化后,和任务线程无异<br />
3)辅助线程 M<br />
主要是负责任务监控、垃圾回收等功能<br />
G可以细分为:<br />
1)任务协程 G<br />
对应用户任务,由go func(){}实现创建<br />
2)第一协程 G0<br />
启动M时,M创建的第一个协程,仅负责协程调度<br />
3)主协程 G<br />
程序启动时,除了创建M0、G0,还会创建出第一个协程,指向runtime.main(最终会调用到main.main)<br />
G的存放位置细分为:<br />
1)P本地队列的runnext(优先存取的位置)<br />
2)P本地队列的runq队列(存放待运行的G,状态是runnable)<br />
3)P本地队列的gfree队列(存放运行完,待回收空间的G)<br />
4)全局队列<br />
2.2调度<br />
协程的调度示意图如下:<br />
![image.png](https://static.studygolang.com/200825/25dcf47a97626d962c59326b010452b0.png)
<br />
该图主要是围绕协程获取、执行两部分讲解,协程执行过程比较复杂,涉及点比较多,简要介绍执行中遇到协程创建、阻塞调用、协程抢占问题。<br />
2.2.1启动<br />
由于启动相对比较简单,故在示意图中没有画出服务启动的流程,概括的讲,程序启动时主要完成:<br />
1)对象创建<br />
完成M0、G0、runtime.main协程的创建<br />
2)资源初始化<br />
主要是确定P的初始数量,默认与核数相等,然后创建P以及栈空间初始化、GC等<br />
感兴趣的同学可以细看汇编源码。<br />
2.2.2协程获取<br />
在图中,协程获取的步骤很清晰:<br />
1)首先是从本地获取,本地是优先取runnext,没有就去runq中读取<br />
2)如果本地获取失败,则去全局队列读取,全局有G的前提下会按照一定公式计算本次拉取的数量<br />
3)如果全局获取失败,则会其他P中偷取(遍历P),在其他P有G的前提下,按照一半的量拉取<br />
4)如果还是获取不到,进入休眠流程,主要是将P放入空闲列表,M依据情况决定是否要继续挣扎下获取G,如果还是获取不到,M就先进入休眠<br />
关于协程获取部分,主要是查看findrunnable函数,该函数实际的处理部分比图上更复杂,感兴趣的同学最好看下源码;补充一点:为了防止全局队列饿死,调度器执行完一定次数(61次)的G后,会优先从全局队列获取,而不是本地优先。<br />
2.2.3协程执行<br />
协程的调度执行过程就是M在G、G0间循环切换的过程,不停的获取G、执行G,其中,循环调度的入口函数是schedule,协程执行函数是execute,当发生调度、切换协程时,最终都会回到schedule函数。<br />
2.2.3.1协程创建<br />
创建协程的处理步骤(入口函数是newproc):<br />
1)从P的gfree中获取一个G对象,如果没有则创建一个G对象<br />
2)对G对象初始化,对象结构中比较关注的是fn(调用函数)和pc(指向goexit)<br />
3)G优先存放在runnext,如果非空,则放入P的runq,如果runq已满,则将runq的前一半和当前G一起放入全局队列中<br />
4)除了存放G,还会当前是否有空闲P,如果有会唤醒继续工作<br />
5)最后是切回,G继续工作<br />
2.2.3.2协程抢占<br />
协程作为用户态下的执行单元,不具备系统级的调度方式,协程整体还是按照顺序调度的,针对某个协程调度时间太久,其他协程得不到调度的情况,Go语言进入了抢占式调度,通过sysmon线程监控、做标记的方式,让运行太久的协程尽快让出CPU:<br />
1)sysmon线程工作原理<br />
sysmon线程会记录所有P的G任务计数schedtick,每执行一个G任务后schedtick递增,如果sysmon检查到某个P的schedtick一直没有递增,则说明这个P一直在执行同一个G任务,如果超过10ms,就在这个G任务的栈信息里面加一个标记,表明可抢占<br />
2)协程抢占<br />
G发生函数调用时,即遇到函数栈帧切换时,进入是否抢占的判断,如果可以抢占,则调度器把这个G添加到全局队列(放入全局是一种对抢占式调度的保护),然后继续执行下一个G<br />
2.2.3.3协程阻塞<br />
协程出现阻塞调用时,如果P中还有待运行的G,则:<br />
1)首先MP分离,P进入空闲状态,调度器会为其寻找M,没有则会新建M,重新组合的MP会继续运行G<br />
2)阻塞调用完后,M首先尝试获取之前的P,如果获取不到,则取空闲P列表中获取,如果还是获取不到,则M进入休眠,G进入全局队列<br />
3.参考<br />
协程调度模型涉及到的点还是比较多的,除了上面提到的,还有GC、Channe等内容需要考虑,正所谓“代码在手、天下我有”,要想搞清楚每个调度细节,还需要多下功夫研读源码,本文的参考资料:<br />
1)https://www.jianshu.com/p/181dc7845bb8<br />
2)https://mp.weixin.qq.com/s/nyTF3IgPf1qkBWCJZQuTuA<br />
有疑问加站长微信联系(非本文作者)