GMP模型
- G:即
goroutine
协程,每个Go关键字都会创建一个协程,协程在当创建的时候大约只占2MB,在不断的运行中可以按需增大(最大1GB)和缩小。 - M:即操作系统线程,Go中默认最多创建10000线程,同时每个Go协程都需要M上才能运行。
- P:即调度器,负责将协程调度到线程上,默认数量为CPU核心数,每一个M都与一个P对应。
本地队列和全局队列
在P调度器中具有两个队列,分别是Local queue本地队列和Global queue全局队列。
每个P调度器都会有一个本地队列,而全局队列则是全局唯一。
当我们使用go关键字创建一个协程时,会将新建的协程优先放置对应P的本地队列中,本地队列是一个基于数组实现的循环链表,最多可以存储256个协程。因此当本地队列存储的协程超过256个时就会将新建的协程放置全局队列中。
work stealing和hand off机制
- work stealing:当M线程所绑定的P调度器的本地队列没有可以运行的G协程认为时,会从其他的P调度器偷取G协程任务,放置进自己的本地队列中
- hand off:当在M线程执行G协程进行系统调用等堵塞时,M线程会释放所绑定的P处理器,将P交给其他空闲的M线程来执行,以便更好的利用CPU的资源。当堵塞的协程恢复后会尝试获得原来所绑定的P处理器,如果获得不到则尝试获得其他空闲的P处理器,如果还获得不到空闲处理器则将当前M线程休眠,随后将G放置进全局队列中。
M0和G0
- M0:它是应用程序启动后编号为0的线程,它负责执行初始化操作和启动一个人G0。
- G0:它是每一个M线程都会创建的第一个G协程,负责调度其他的协程。
Go调度大致流程
- 创建第一个M线程 M0
- 创建第一个G协程 G0
- 进行初始化操作,例如设置M线程的最大个数、初始化栈和内存、设置GOMAXPROCS参数
- 创建main函数的G协程
- 启动M0
- M不断获得P中的G来执行相关任务
M会优先会所绑定的P中的本地队列获取G,当本地队列为空时,就会尝试从全局队列里获得一批协程,如果还为空最会使用上面所讲的work stealing策略
,如果还为空则会自旋或者进入休眠状态。
同时一个协程在运行中最多占用CPU 10ms的时间,以让其他的协程也有运行的机会。
一道GMP面试题
1 | func TestGMP() { |
上面的代码的输出顺序一直是固定的,这是因为我们使用runtime.GOMAXPROCS(1)
将P调度器的数量设为1,这样所有G协程都会进入该P调度器。
当一个协程进入P处理器时,都会先将该处理器放置runnext
(即下一个运行的协程),如果runnext
已有协程的话,就会将之前的协程放置进本地队列中,然后再将新的协程放进runnext
中。
因此j为9的协程总是在runnext的位置,即第一个运行的协程,随后其他的协程根据队列FIFO的顺序,陆续被执行。