Golang GMP模型 笔记
https://www.yuque.com/aceld/golang/srxd6d
https://www.bilibili.com/video/BV19r4y1w7Nx/?p=18
本篇文章只是这个视频的一个笔记,但视频其实有非常多让人困惑的地方并没有做解释,本文也没有深究
核心在于协程调度器的优化
什么是GMP模型?
-
G(goroutine):协程
-
P(Processor):协程处理器,一次只能处理一个协程,每个P都有一个协程队列,用于存放待处理的协程。P的数量可以通过环境变量或代码进行设置,它的数量表示Golang在某时刻支持的最大并行量
-
M(Thread):内核态线程
另外,还有一个全局G队列,当P队列都满了就会放到全局G队列中。
Golang调度器的设计策略
1. 线程复用
work stealing机制:当某个P队列空闲时,它会尝试从其他P队列中拉取G过来执行
hand off机制:正常情况下,一个P和一个M相绑定执行,但是当P正则执行的G发生阻塞时(例如执行read操作),它会唤醒一个新的M并将P绑定到这个新的M上执行,而原来那个M就专门负责执行阻塞的G ,如果G阻塞被唤醒了,就会重新加入到某个P队列中
2. 利用并行
可以指定多个P,充分利用多核cpu
3. 抢占
goroutine也会存在类似进程抢占cpu的机制(不是使用的队列吗?怎么也是抢占式的)
4. 全局队列
如果空闲P无法从其他P队列中stealing G,则会尝试从全局队列中stealing一个G,这一过程涉及全局队列的加锁解锁,效率不高
M0 和 G0
go程序启动后,go的进程会创建第一个线程M0,M0 也创建一个G0用于创建Golang的协程环境,例如创建P和队列等。
每个P被创建时都会绑定一个G0用于G队列的调度,G0也会负责该P队列中所有G的调度,它不会放到P队列中,当一个G执行完成后,P会先加载其对于的G0,再由G0去它的P队列中取出G来给P执行
当开始执行main函数时,就会创建一个新的G加入到某个P队列中用于执行main的内容,当碰到go语句时,也会创建G添加到P队列中,如果P队列都是满的则会加入到全局队列中
简单来说,M0就是用来做初始化环境工作的,G0主要负责P队列中G的调度
golang中可以使用 trace 或者 GODEBUG 来查看GMP的调度信息
创建G
创建时有如下几种场景或机制:
局部性:如果某正则执行的G创建了一个新的G,很多情况下,它们可能存在一些共享资源,所以新建的G应该优先放到创建它的G所在的那个P队列
队列满:一个P队列容量是有限的(默认4G内存),如果它执行的某个G创建了非常多的G,根据局部性原则,这些新建的G都应该放到这个P队列中,如果P队列已经满了的情况下又新建了一个G,则Golang会将该P队列的前面一半G连同新建的G一起打乱顺序放到全局队列中,后一半的G就会向前移动,此时P队列就空出后一半的容量,如果再有新建的G就可以向P队列后面放,再满的话重复上述过程
自旋线程:每当一个G在创建另一个G时,它都会尝试唤醒休眠线程中的一个线程,当一个线程被唤醒后,它会绑定一个P,并执行它的G0,不过由于此时它的P队列是空的,所以它没有任务执行,它就会忙循环,称为自旋线程。自旋的同时,它也会尝试从全局队列中拉取G,但它会一次拉取多个(拉取数量 = min(全局队列长度/p数量+1,全局队列长度/2)),并停止自旋。该过程称为全局队列到P本地队列的负载均衡
stealing:当一个P队列执行完了,且全局队列也是空的,它又成为了自旋线程,此时它就会尝试从其他P队列中偷后面一半的G
自旋线程限制:自旋线程+执行线程 <= $GOMAXPROCS,当已经达到最大限制后,新创建的M就会放到休眠线程队列中,因为已经没有P可以和它绑定了
如果G8发生阻塞,P2就会重新去休眠线程队列中唤醒一个队列进行绑定,而M2负责等待G8阻塞结束
当G8阻塞结束后,它并不能直接在M2上执行,因为G必须在P上才能执行
此时,M2就会尝试 1)绑定原来的P2,但此时P2已经绑定了M5,所以失败。2)从空闲P队列中绑定一个P,但此时空闲P队列是空的,所以G8就会被加到全局队列,M2会进入休眠线程队列