1.go的GMP模型
goroutine运行在用户态,是由runtime来控制调度的,调度过程中主要涉及到三个对象:G-go关键字启动的协程(也有内部启动的协程如g0)、M-运行G的系统线程(由操作系统控制调度)、P-资源处理器,用来管理G的,M必须绑定一个P才能运行其协程队列里的G。具体的调度过程是这样的:
程序启动时创建GOMAXPROCS个P,当有go关键字创建G时会优先加入到当前P的本地队列中(缓存局部性原理),当本地队列满了调度器会调度部分协程到全局队列中,此时P去线程池唤醒一个休眠的M(如没有则新创建一个M)进行绑定,M获取到P本地队列中的G开始执行,当本地队列里面的G执行完则先去全局队列获取,若获取不到则去其它队列偷取,若都没有则当前M进入自旋状态(因为线程的创建销毁唤醒比较消耗CPU,但是自旋线程本质是线程在运行不过没有执行G,因此也有数量限制,超过限制M休眠),不断的循环寻找等待新的G。
2.怎么防止G全局队列饥饿
极端情况下所有本地队列的G被执行数和生产新的G数量保持“平衡”,导致全局队列里的G饥饿无法被执行。go调度器为了公平起见,每执行61次G后会直接去全局队列取1个G。
//go1.18.4/src/runtime/proc.go:3175
// Check the global runnable queue once in a while to ensure fairness.
// Otherwise two goroutines can completely occupy the local runqueue
// by constantly respawning each other.
if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp = globrunqget(_g_.m.p.ptr(), 1)
unlock(&sched.lock)
}
3.G、M、P的个数
- golang创建协程内存消耗较小(2K左右,理论上个数受内存大小限制),加上G在退出时会把G清理掉后放入本地或者全局空闲列表的gfree队列中以方便复用,配置一般的机器也能开启十万级 goroutine。
- M线程的个数默认是10000个,线程相较G耗资源,一般达不到该数量,M必须绑定一个P才能执行G,所以M的数量一般大于等于P的数量。
- P的个数由GOMAXPROCS控制,默认为CPU的核数。
4.go的协程调度策略,具体如何抢占的
进程由操作系统调度,分为抢占式调度和非抢占式调度(FCFS),抢占式调度有先到先得(FIFO)、最短任务优先(SJF)、最短完成时间任务优先(STCF)、时间片轮转(Round-Robin, RR)等策略。操作系统调度策略
协程由runtime在用户态进行调度的,采用时间片算法的方式,公平,可以防止少数协程长时间执行导致其它协程饥饿。
具体的抢占过程是基于信号的方式:程序运行时守护线程sysmon会去监控执行所有G的时间,如果G超过了10ms,sysmon会给当前的M发送一个sigurg信号,M中断执行该G(放入全局队列),去获取新的G;当调用syscall时间太长时也会发送sigurg信号。
5.GMP模型,发生了阻塞怎么办,如何保证G不会被其他G的系统调用、网络调用阻塞
- M系统线程发生系统调用阻塞
1.如G打开文件时,M与P解绑并阻塞进入等待状态,G也进入等待状态,此时P唤醒一个M或者创建一个新M继续执行后面的G;
2.当系统调用结束,M优先和旧P进行绑定继续执行G(亲和性、局部性);如果旧P正在被使用并且有空闲的P,则新P和M绑定,刚才阻塞的G加入其本地队列,M继续执行G;如果没有空闲的P,G加入到全局队列中,等待被其它的P调度,然后M将进入缓存池休眠。- G协程阻塞
1.G读写channel时阻塞:此时MP不解绑(M继续执行后面的G),而G加入到channel的recvq或sendq队列中进行等待,当有数据写channel或有读channel时唤醒对应的G,加入P的本地队列或者全局队列等待执行。
2.G发起网络请求时阻塞:比如当创建完一个Listener,调用Accept开始接收客户端连接。如果没有对应的请求,那么最终会把G放入到pollDesc的rg;
如果是一个conn类型的fd等待可写I/O,那么会把G放入到pollDesc的wg中。rg、wg都是指针类型,实际这两个字段存储的就是Go底层的G,更具体点是等待i/O ready的G。
网友评论