Go 调度模型(三)
别抱怨,也别自怜,所有的现状都是你自己选择的
前面的章节中,我们介绍了操作系统的调度模型:N:1
、1:1
、M:N
。而Go
采用了更高效的方式M:N
。从进程的角度来说,线程是最小的调度单元。而Go
的模型下,可以把P
作为最小单元的调度单元,即在单个线程上运行的Go
代码。
执行模型
下图展示了Go
的最小调度单元模型。其中的有两个线程,各维持一个P
对象,而且正在执行一个G
代码。为了运行G
,M
必须首先持有P
对象。图中灰色的G
表示还没有被执行,等待被调度。它们被组织在P
的一个runqueues
的队列中,当M
创建新的Goroutine
时,对应的G
就会被追加到该队列的末尾。
阻塞模型
为什么要引入P
结构,直接将runqueues
放在M
中,不就可以摆脱P
了吗?当然不行,它存在的意义还在于:当M
因为其他原因被阻塞时,我们需要将runqueues
中的G
交给别的M
来继续处理。因为线程不可能既执行代码,又阻塞在系统上。
如上图所示,当M0
阻塞在系统调用上时,它会放弃自己的P
,以保证M1
可以继续执行其他G
。当M0
系统调用返回时,M0
为了继续执行G0
,就必须尝试重新获取P
对象。正常的执行流程是:它尝试去偷其它线程的P
,如果不行,就将G0
放到全局的runqueues
中,之后进入休眠。
当P
本地的runqueues
运行完之后,M
会去全局队列取G
来执行。同时,全局队列的G
也会被间歇性检查,否则里面的G
可能永远都得不到执行了。
偷G
模型
当runqueues
分布不均衡时,可能存在其中一个M
执行完了本地的P
,而其他P
的本地队列还有很多G
等待被执行。如图所示,为了去继续运行Go
代码,P1
首先会尝试去全局队列获取。如果全局队列没有,那么它就会随机从别的P
去偷一半回来。这样做也是用来保证所有线程都一直有工作可以做。
参考文章: