Fuhui

Go 调度模型(三)


别抱怨,也别自怜,所有的现状都是你自己选择的

前面的章节中,我们介绍了操作系统的调度模型:N:11:1M:N。而Go采用了更高效的方式M:N。从进程的角度来说,线程是最小的调度单元。而Go的模型下,可以把P作为最小单元的调度单元,即在单个线程上运行的Go代码。

执行模型

下图展示了Go的最小调度单元模型。其中的有两个线程,各维持一个P对象,而且正在执行一个G代码。为了运行GM必须首先持有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去偷一半回来。这样做也是用来保证所有线程都一直有工作可以做。

参考文章:

  1. The Go scheduler