Go语言学习——Go语言的GMP调度模型整理

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

有疑问加站长微信联系

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:1006366459

252 次点击  ∙  1 赞  
加入收藏 微博
4 回复  |  直到 2020-08-26 10:54:46
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传