一、前言
Golang语言的关键点之一:协程;相对于线程而言,协程是一个更轻量级的线程。在讲到协程的时候,不可避免的要讲到GMP模型,而GMP模型的前身是GM模型。
二、GM与GMP
Go1.1版本(2012年)之前用的都是GM模型,但是由于GM模型性能不好,之后官方对调度器进行了改进,演化为现在的GMP模型。
GM模型
GM模型中的G全称为Goroutine协程,M全称为Machine内核级线程,其调度过程如下:
M(内核级线程)从加锁的Goroutine队列中获取G(协程)执行,如果G在运行过程中创建了新的G,那么新的G也会被放在全局队列中。
基于以上设计存在两个不足之处:
- 调度,返回G都需要获取队列锁,形成了激烈竞争。
- M转移G没有把资源最大化利用。比如当M1在执行G1时,M1创建了G2,为了继续执行G1,则需要把G2交给M2执行,因为G1和G2是相关的,而寄存器中会保存G1的信息,因此G2最好是放在M1上执行,而不是其他M。
GMP模型
GMP模型是在GM模型基础上增加了一个P层。
G:goroutine,协程
M:machine,内核级线程
P:processor,协程运行所需要的资源。
全局队列:当P中的本地队列有G溢出时,会被放在全局队列中;
P的本地队列:P内置的G队列存在数量限制(≤256个)。 此处有
- 当队列P1中的G1在运行过程中创建G2,G2会优先被放在P1本地队列中,如果P1本地队列满了,则会把P1队列中一半的G移动到全局队列中;
- 如果P的本地队列为空,那么他会先到全局队列中获取G,如果全局队列中也没有G,则尝试从其他线程绑定的P中偷取一半的G
P与M的数量是不能无限扩增的:
P的数量:由启动时环境变量($GOMAXPROCS) 或者由runtime的方法GOMACPROCS()决定。
M的数量:go程序启动时,会设置M的最大数量,默认为10000。但是根据内核很难创建出这麽多的线程,因此在默认情况下的最大数量取决于内核;也可以调用runtime/debug中的SetMaxThreads函数,手动设置M的最大数量。
P与M的创建时机:
P:在确定P的最大数量(n)后,运行时系统会根据这个数量创建n个p,即运行时创建。
M:内核级线程的初始化是由内核管理的,当没有足够的M来关联P 并运行其中可运行的G时,会请求创建新的M。
比如:M在运行G1时被阻塞住了,此时需要新的M去绑定P,如果没有在休眠的M则需要创建新的M。
G在GMP模型中的执行过程
- 调用go func()创建一个goroutine;
- 新创建的G有限保存在P的本地队列中,如果本地队列满了,则会保存在全局队列中;
- M需要在P的本地队列中获取一个可用G,如果P的队列为空则会先从全局队列获取G,如果全局队列也为空则去其他P对队列中偷取G放在自己的P中;
- G将相关参数传输给M,为M执行G做准备;
- 当M执行某个P时,如果发生系统调用产生导致M会阻塞;如果当前P队列中有一些G,runtime会将线程M和P分离,然后再获取空闲的线程或者创建一个新的M(内核级线程)来服务这个P,阻塞调用完成后G被销毁,将值返回;
- 销毁G,将执行结果返回;
- 当M系统调用结束时,这个M会尝试获取一个空闲的P执行,如果获取不到P,则这个M变成休眠状态,加入到空闲线程中。
三、两者区别
-
GMP相对于GM做的优化点有三个:
- 每个P都有自己的本地队列,而不是所有的G操作都经过全局队列,这样锁的竞争会少很多。(GM模型性能开销最大的就是锁竞争)
- P的本地队列平衡上实现了Work Stealing算法。如果P的本地队列为空则会从全局队列或者其他P队列中窃取可运行的G来运行(通常偷一半),减少空转,提高资源利用率。
- hand off机制。当M0线程因为G1进行系统调用阻塞时,M释放绑定的P,把P转移到其他空闲的M执行,同样提高了资源利用率。