G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系 。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空 , 就会想其他的MP组合偷取一个可执行的G来执行;
一个M调度G执行的过程是一个循环机制;会一直从本地队列或全局队列中获取G
上面说到P的个数默认等于CPU核数 , 每个M必须持有一个P才可以执行G,一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用 。类似线程池 , Go也提供一个M的池子,需要时从池子中获取,用完放回池子 , 不够用时就再创建一个 。
work-stealing调度算法:当M执行完了当前P的本地队列队列里的所有G后 , P也不会就这么在那躺尸啥都不干,它会先尝试从全局队列队列寻找G来执行 , 如果全局队列为空,它会随机挑选另外一个P,从它的队列里中拿走一半的G到自己的队列中执行 。
如果一切正常,调度器会以上述的那种方式顺畅地运行,但这个世界没这么美好,总有意外发生,以下分析goroutine在两种例外情况下的行为 。
Go runtime会在下面的goroutine被阻塞的情况下运行另外一个goroutine:
用户态阻塞/唤醒
当goroutine因为channel操作或者network I/O而阻塞时(实际上golang已经用netpoller实现了goroutine网络I/O阻塞不会导致M被阻塞,仅阻塞G , 这里仅仅是举个栗子),对应的G会被放置到某个wait队列(如channel的waitq),该G的状态由_Gruning变为_Gwaitting , 而M会跳过该G尝试获取并执行下一个G,如果此时没有可运行的G供M运行,那么M将解绑P,并进入sleep状态;当阻塞的G被另一端的G2唤醒时(比如channel的可读/写通知),G被标记为,尝试加入G2所在P的runnext(runnext是线程下一个需要执行的 Goroutine 。),然后再是P的本地队列和全局队列 。
系统调用阻塞
当M执行某一个G时候如果发生了阻塞操作 , M会阻塞,如果当前有一些G在执行,调度器会把这个线程M从P中摘除 , 然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P 。当M系统调用结束时候 , 这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列 。如果获取不到P,那么这个线程M变成休眠状态, 加入到空闲线程中,然后这个G会被放入全局队列中 。
队列轮转
可见每个P维护着一个包含G的队列 , 不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度 。
除了每个P维护的G队列以外 , 还有一个全局的队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G 。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死 。
除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G 。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死 。
M0
M0是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量rutime.m0中 , 不需要在heap上分配,M0负责执行初始化操作和启动第一个G , 在之后M0就和其他的M一样了
G0
G0是每次启动一个M都会第一个创建的goroutine,G0仅用于负责调度G,G0不指向任何可执行的函数,每个M都会有一个自己的G0,在调度或系统调用时会使用G0的栈空间,全局变量的G0是M0的G0
推荐阅读
- 录屏直播缺点,录屏直播缺点有哪些
- 圣诞树代码教程java,圣诞树代码教程html
- sqlserver中的via协议,sqlserver没有via协议
- c语言中函数有什么特点 c语言中函数有什么特性
- window虚拟机ios下载地址,windows虚拟机苹果版
- 苹果电脑怎么注册chatgpt,苹果电脑怎么注册ID
- 路由器的ssid怎么关闭,路由器ssid关闭后怎么联网
- python-自相关函数 python计算自相关系数
- 包含redis新功能的词条