golang语言调度模式:G、M、P,三者配合完成协程调度,协程实现轻量级并发模型,线程切换需要在内核态与用户态切换、消耗资源,协程属于用户层的调度模型,提升并发效率。改文章不适合新手,而是对于go调度有一定的理解。
M:真正的线程,用于真实执行用户程序的单元,协程想要运行必须绑定M才能执行。
G:协程,用户层的并发模型单元,也是用户层调用go关键字,形成的执行单元。
P:逻辑概念,在原有的go语言模型中没有P,将导致G的push、pop、状态变更需要全局加锁,所以增加P的概念,P下面挂载goutine,对于goutine的push、pop等操作,只在P内部加锁,降低锁的颗粒度。这个特别像分布式ID的一种生成方式,先从全局ID集合获取一个号段,在本号段内使用ID资源是不需要加锁的。
1.逻辑概念P需要维护什么?
runq
: P需要依靠M运行,才能够运行P下的G,所以P下包含可运行runnable的G列表。
gFree
: P上的G执行完成后,一般不直接回收,否则其他用户层程序再次创建G,还需要重新分配。所以P上维护已经处于dead状态的G。
status
:P也有状态,包括空闲idle
、运行running
、系统调用syscall
、停止stop
等状态。
2. 定义基础流程。
1)gogo流程
,执行某个go协程,将pc、堆栈信息拷贝到当前调用栈,调用go协程。
2)schedule
流程,从当前P中寻找可用的g,找不到从全局runq找…经过一系列寻找(寻找流程定义为:findRunnable
),找到runnable状态的g后,调用gogo
流程调度。
3)findRunnable
流程,从当前P的runq找、从全局sched的runq找,以及从网络等待返回的g找,如果都找不到待执行的g,表示系统没事干,将p与m分离,设置p为idle状态,休眠当前M,等待有活干 再唤醒M。
3. 创建协程、调度协程、退出协程
3.1 创建协程
1)优先从当前p的gFree里面找一个g,如果没有则新建g,创建g数据结构,并分配堆栈信息。
2)设置好函数的pc等信息,将g状态由dead 改为runnable。
3)将g放入p的runnable队列。
问题:
假设起初系统非常闲,只有一个P 一个M 一个主程序G在运行,此时其他P是idle状态,其他的M 都是休眠状态。当用户层创建G,经过上面三个步骤,则当前P下面有两个runnable的G,而其他P还在idle状态,这是无法利用并发能力。
答:创建协程还有最后一步wakep,该函数找一个idle状态的p,再找一个休眠状态的m(如果没有则新建),唤醒m后参与调度。经过wakep操作,新调度的p会从原有的p下面挂载的两个G 偷取一个,这样两个P都在并发工作。
3.2 调度协程
1)当某个P执行完g后,可以调度到执行当前的g。
2)刚好当前g调用系统函数,将当前g设置为syscall状态,并设置栈溢出检查,并将p与m分离,注意此时m未与p分离,仅仅p将与m有关的内存清空。并将当前p设置为syscall状态。此时并没有将p与g分离,而且p已经syscall状态。一个8核的CPU,有8个P,如果每个P发生系统调用,所有的p处于syscall状态,那系统将无法继续运行。此时需要咋搞?
3)go有个监控线程,负责检查调度抢占。如果p上的g运行时间太久,则发生抢占。如果p是syscall状态,并且该状态持续一段时间时间,且p上有g要运行,则将p状态改为idle,并且启动启动新的M(或idle状态的M)运行该P。
4)如果系统调用很快完成,并没有触发调度抢占策略。首先判断刚才执行g的p是否还是syscall状态,也就是是否被其他g使用,如果没有还在等待上一次系统调用结果,那该p直接调度g。上面说过p此时已经与m分离了,但g上扔保存m相关信息,需要赋值给p。设置p为running状态。
5)如果此时执行系统调用的p已经被调度到其他m上,干其他活。如果刚好有空闲的p,则将p设置为running状态,运行该g。
6)如果此时也没有空闲p,那没办法,将该g转为runnable状态,将g绑定的m停止,因为该m一直陪着g等待系统调用返回,没做其他事情。后调用调度流程,开始新的调度。
3.3 退出协程
1)将g状态改为dead。
2)将g放入到p的gFree列表中。
3)触发下一次调度流程。