go语言中的GMP模型和GM模型浅析介绍
G: Goroutine
代表Go 协程Goroutine,存储了 Goroutine 的执行栈信息、Goroutine 状态以及 Goroutine 的任务函数等。G的数量无限制,理论上只受内存的影响,创建一个 G 的初始栈大小为2-4K,配置一般的机器也能简简单单开启数十万个 Goroutine ,而且Go语言在 G 退出的时候还会把 G 清理之后放到 P 本地或者全局的闲置列表 gFree 中以便复用。
M: 线程
Go 对操作系统线程(OS thread)的封装,可以看作操作系统内核线程,想要在 CPU 上执行代码必须有线程,通过系统调用 clone 创建。M在绑定有效的 P 后,进入一个调度循环,而调度循环的机制大致是从 P 的本地运行队列以及全局队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 goexit 做清理工作并回到 M,如此反复。M 并不保留 G 状态,这是 G 可以跨 M 调度的基础。M的数量有限制,默认数量限制是 10000,可以通过 debug.SetMaxThreads() 方法进行设置,如果有M空闲,那么就会回收或者睡眠。
P: Processor 本地队列
虚拟处理器,M执行G所需要的资源和上下文,只有将 P 和 M 绑定,才能让 P 的 runq 中的 G 真正运行起来。P 的数量决定了系统内最大可并行的 G 的数量,P的数量受本机的CPU核数影响,可通过环境变量$GOMAXPROCS或在runtime.GOMAXPROCS()来设置,默认为CPU核心数。
Sched:调度器结构,它维护有存储M和G的全局队列,以及调度器的一些状态信息
一.GM模型:
2012年前的调度器模型,使用了4年果断被抛弃,缺点如下:
1. 创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争。
2. M转移G会造成延迟和额外的系统负载。
比如当G中包含创建新协程的时候,M创建了G’,为了继续执行G,需要把G’交给M’执行,也造 成了很差的局部性,因为G’和G是相关的,最好放在M上执行,而不是其他M'。
3. 系统调用(CPU在M之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销。
二.GMP模型:
P的数量:
由启动时环境变量`$GOMAXPROCS`或者是由`runtime`的方法`GOMAXPROCS()`决定
M的数量:
go语言本身的限制:
go程序启动时,会设置M的最大数量,默认10000.但是内核很难支持这么多的线程数
runtime/debug中的SetMaxThreads函数,设置M的最大数量
一个M阻塞了,会创建新的M。
P何时创建:
在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P。
M何时创建:
没有足够的M来关联P并运行其中的可运行的G。比如所有的M此时都阻塞住了,而P中还有很多就绪任务,就会去寻找空闲的M,而没有空闲的,就会去创建新的M。
全场景解析:
1.P拥有G1,M1获取P后开始运行G1,G1创建了G2,为了局部性G2优先加入到P1的本地队列。
2.G1运行完成后,M上运行的goroutine切换为G0,G0负责调度时协程的切换。
从P的本地队列取G2,从G0切换到G2,并开始运行G2。实现了线程M1的复用。
3.假设每个P的本地队列只能存4个G。
G2要创建了6个G,前4个G(G3, G4, G5, G6)已经加入p1的本地队列,p1本地队列满了。
4.G2在创建G7的时候,发现P1的本地队列已满,
需要执行负载均衡(把P1中本地队列中前一半的G,还有新创建G转移到全局队列),
这些G被转移到全局队列时,会被打乱顺序
5.G2创建G8时,P1的本地队列未满,所以G8会被加入到P1的本地队列。
6.在创建G时,运行的G会尝试唤醒其他空闲的P和M组合去执行。
假定G2唤醒了M2,M2绑定了P2,并运行G0,但P2本地队列没有G,M2此时为自旋线程
7.M2尝试从全局队列取一批G放到P2的本地队列,至少从全局队列取1个g,
但每次不要从全局队列移动太多的g到p本地队列,给其他p留点。
8.假设G2一直在M1上运行,经过2轮后,M2已经把G7、G4从全局队列获取到了P2的本地队列
并完成运行,全局队列和P2的本地队列都空了,那m就要执行work stealing(偷取):
从其他有G的P哪里偷取一半G过来,放到自己的P本地队列。P2从P1的本地队列尾部取一半的G
9.G1本地队列G5、G6已经被其他M偷走并运行完成,当前M1和M2分别在运行G2和G8,
M3和M4没有goroutine可以运行,M3和M4处于自旋状态,它们不断寻找goroutine。
系统中最多有GOMAXPROCS个自旋的线程,多余的没事做线程会让他们休眠。
10.假定当前除了M3和M4为自旋线程,还有M5和M6为空闲的线程,
G8创建了G9,G8进行了阻塞的系统调用,M2和P2立即解绑,P2会执行以下判断:
如果P2本地队列有G、全局队列有G或有空闲的M,P2都会立马唤醒1个M和它绑定,
否则P2则会加入到空闲P列表,等待M来获取可用的p。
11.G8创建了G9,假如G8进行了非阻塞系统调用。M2和P2会解绑,但M2会记住P2,
然后G8和M2进入系统调用状态。当G8和M2退出系统调用时,会尝试获取P2,
如果无法获取,则获取空闲的P,如果依然没有,G8会被记为可运行状态,
并加入到全局队列,M2因为没有P的绑定而变成休眠状态
相关文章