GMP模型
GMP模型的演进过程
1)GM模型
在Go1.0版本是Go的调度方式为GM模式,但是其有几个严重不足:限制了Go并发编程的的伸缩性
单一全局互斥锁和集中状态存储的存在导致所有goroutine相关操作都要上锁
goroutine的传递问题:经常在M之间传递“可运行”的goroutine回导致调度延迟增大,带来额外的性能损耗
每个M都做内存缓存,导致内存占用过高,数据局部性较差。
因系统调用而形成的频繁的工作线程阻塞和解除阻塞会带来额外的性能损耗
2) GMP模型
Go1.14 版本中加入了基于系统信号的goroutine抢占式调度机制,很大程度上解决了goroutine“饿死”的问题。
GMP 模型详解
1. G、M、P的概念
G: 代表goroutine,存储了goroutine的执行栈信息,goroutine状态以及goroutine的任务函数等。G对象是可以重用的
P: 代表逻辑processor,P的数量决定了系统最大可以并行执行的G的数量(系统的物理内存的CPU核数>= P的数量)。P中最有用的是其拥有各种G对象列表、链表一些缓存和物理状态。
M: M代表真正执行计算资源。在绑定了有效的P之后,进入一个调度循环;而调度循环的机制大致是从各种队列、P的本地运行队列中获取G,切换到G的执行栈上并执行G的函数,调用goexit做清理工作并返回到M。
2. G被抢占调度
在Go程序启动时,运行时会启动一个名为sysmon的M(一般称为监控线程)。该M无需绑定P即可运行。在整个Go程序的运行过程中主要完成如下工作
释放闲置超过5分钟的span物理内存
如果超过2分钟没有垃圾回收,强制执行
将长时间未处理的netpoll结果添加到任务队列
向长时间运行的G任务发出抢占式调度
回收因系统调用长时间阻塞的P
当一个G任务运行超过10ms,监控线程就会认为其运行时间太久发出抢占式调度的请求。一旦G的抢占标志位被设置为true,**那么在这个G下一次调用函数或方法时,runtime便将G抢占并移除运行队列,放入P的本地执行队列。**若P的本地执行队列已满,将会放在全局队列。等待下一次的调度。
3. channel阻塞或者网络I/O情况下
如果G被阻塞在某个channel操作或者网络I/O操作上,那么G会被放置到某个等待队列中,M会尝试运行P的下一个可运行的G。如果此时P没有可运行的G供M运行,那么M将解绑P并进入挂起状态。当I/O操作或者channel完成时,处于等待队列的G会被唤醒,标记为runnable,并被放入某个P的队列中,绑定一个M后继续执行。
4. 系统调用阻塞情况下的调度
若发生系统调用情况下的阻塞,此时的G会被阻塞,而且执行该G的M也会阻塞(和P解绑-------实际上是被sysmon抢走)。此时的P中如果还有待执行的G,那么P会继续寻找空闲M来执行,若没有空闲的M,则会创建一个新的M。
//**如果P中没有可执行的G,那么P是会和G、M一起阻塞还是会变成空闲状态**
当系统调用返回后,阻塞在该系统调用上的G会尝试获取一个可用的P,如果存在可用的P,之前运行该G的M将绑定P继续执行G;假如没有可用的P,那么M会和G解绑,将G放入全局的运行队列中。
//**这里的可用的P是指没有和M绑定的空闲的P吗**
//**当没有可用的P时,M和G解绑之后,M会被回收还是睡眠**
GMP调度器设计策略
1. 复用线程:
避免频繁的创建,销毁线程,而是对线程的复用。 包括work stealing 机制以及hand off机制
work stealing 机制:当本地线程无可用的G时,先从全局队列中获取,如果全局队列中也没有的话,就从其他线程绑定的P中偷取G
hand off机制 :当本地线程因为系统调用而阻塞时,线程释放绑定的P,把p转移给其他空闲的线程执行。
2.利用并行:
使用GOMAXPROCS设置P的数量
3. 抢占
一个goroutine最多占用内存10ms ,就会被监控线程将抢占标志位置为true,在此goroutine下次调用函数方法时就会被抢占,将其放入本地队列等待下一次调用。
4.全局G队列
当M绑定的P中没有可执行的G时,就会去全局G队列中找,如果还没有,就会去别的P中偷取。