G :goroutine协程
P :processor处理器,线程的执行上下文,P的数量决定了Golang的执行并发度,启动时环境变量$GOMAXPROCS
或者是由runtime
的方法GOMAXPROCS()
决定
M:thread线程,可以认为是真正的计算资源,可以认为它是系统线程,当M绑定P时就可以进入循环调度,但M不会特别保存G状态,提供了G跨越M调度的基础
Sched:代表调度器,它维护有存储M和G的队列以及调度器的一些状态信息等(未在图中给出)
全局队列(Global Queue):存放等待运行的G。
P的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建G'时,G'优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。
P列表:所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS
(可配置)个。
M:内核级线程,线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。
Goroutine调度器和OS调度器是通过M结合起来的,每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行。
GPM的调度过程:
新创建的Goroutine会先存放在Global全局队列中,等待Go调度器进行调度,随后Goroutine被分配给其中的一个逻辑处理器P,并放到这个逻辑处理器对应的Local本地运行队列中,最终等待被逻辑处理器P执行即可。在M与P绑定后,M会不断从P的Local队列中无锁地取出G,并切换到G的堆栈执行,当P的Local队列中没有G时,再从Global队列中获取一个G,当Global队列中也没有待运行的G时,则尝试从其它的P窃取部分G来执行相当于P之间的负载均衡
P和M之间的关系?
p和m没有绝对的数量关系,但在工作时一个m一定绑定一个p,可以把p理解为m和g之间的桥梁,当m阻塞时,p会去创建m或者切换到另一个m,所以,即使P的默认数量是1,也可能会创建很多m
P和M何时创建?
在程序最开始,确定p的数量n以后就开始创建n个p,一个p会绑定一个m,当前所有的m都阻塞了,而p中还有很多就绪任务,就会去寻找空闲的m,如果没有空闲m,就会去创建一个新的m
GPM调度的优点及其其中的运行机制?
设计策略:避免了频繁的创建、销毁线程,而是对线程的复用,需要保持足够多的运行工作线程充分利用硬件并行性与运行过多的线程之间取得平衡,因此需要调度器管理何时启动m,何时停掉m
1)work stealing 机制
当m没有可用的g时(本地队列),尝试从其他线程绑定的p中偷取G,如果从其他p偷不到G时,尝试从全局G队列获取G,而不是销毁线程
2)hand off 机制
当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行
3)充分利用并行
GOMAXPROCS决定p的数量,最多GOMAXPROCS个线程分布在多个CPU并行
4)抢占
Golang中一个Goroutine最多占用CPU10ms,一旦达到,强制收回p,并重尝试拿p,进行调度循环