GMP模型
什么是GMP模型?
- G:gorotine(协程)
- M:machine(内核线程)
- P:processor(调度器)
Go语言运行时,通过核心元素G,M,P 和 自己的调度器,实现了自己的并发线程模型。调度器通过对G,M,P的调度实现了两级线程模型中操作系统内核之外的调度任务。
Golang调度器由来

Goroutine调度器器的GMP模型的设计思想
GMP模型简介
- G:goroutine(协程)
- M:machine(内核线程)
- P:processor(调度器,负责G和M之间的调度,不是GMP模型的调度)

全局队列:存放等待运行的G
P的本地队列:
- 存放等待运行的G,
- 有数量限制(不超过256个),
- 新创建的G优先放置在本地队列,存满了会放在全局队列
P列表:程序启动时创建,默认数量为cpu线程数
M列表:当前操作系统分配到当前Go程序的内核线程数
P和M的数量:
- P的数量可通过环境变量($GOMAXPROCS)或程序中代码设置
runtime.GOMAXPROCS(). - M的数量Go语言本身限制为10000,可通过runtime/debug包中的
SetMaxThreads函数来设置, - 有一个M阻塞,就会创建一个新的M,有M空闲就会回收或者睡眠。
调度器的设计策略
1.复用线程
避免频繁的创建销毁线程,有两种机制 work stealing机制和hand off机制
work stealing机制 : 当前P的本地队列和全局队列均无可运行的G时会尝试从其他P的队列中偷取G
hand off机制 : 当本线程正在运行的G发生阻塞时,会将G和M进行绑定,并把P转移给其他的空闲M
2.利用并行
GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个cpu上同时运行
3.抢占
在coroutine中要等待一个协程主动让出CPU才执行下一个协程.
在Go中,一个goroutine最多占⽤用CPU 10ms,防⽌止其他goroutine被饿死.
4.全局G队列列
用于存放G,有锁保护,全局队列中的G是所有P共享的,当P的本地队列为空时,会从全局队列中获取G
调度规则
如果处理器没有任务可处理,它会按以下规则来执行,直到满足某一条规则:从本地队列获取任务 -> 从其它的处理器的本地队列窃取任务 -> 从全局队列获取任务
- 为什么优先从其他 P 偷取任务?
减少全局锁竞争: 全局队列是一个全局共享资源,访问它时需要
加锁,可能会引发竞争。而本地队列之间没有共享,因此偷取任务时不会引发全局锁的竞争,能够提高性能。
负载均衡: 偷取其他 P 的任务有助于在多个处理器之间更好地均衡任务负载,减少单个处理器繁忙而其他处理器空闲的情况。
go func() 经历了什么

- 先创建一个goroutine 也就是G
- G会优先 存储在创建G的P的本地本地队列,若队列已满就会存在全局队列
- G只能P的调度下运行在M上,M和P一一绑定,当G执行关闭后会在本地队列中取一个G运行,若队列为空,就会从全局队列取一个G,全局队列也为空的话就会从其他绑定的M和P的队列中偷取
- M调度G的执行过程是一个循环机制
- 当M执行某个G的时候,如果发生了syscall或其他阻塞操作,M会阻塞,如果当前有一些G在运行,runtime就会把这个M与P进行绑定,然后去复用空间的线程或创建一个线程来服务这个P
- 当阻塞的M不再阻塞时,G会尝试获取一个空闲的P执行并放入其本地队列,获取不到P的话,这个M就会变成休眠状态,加入到空闲线程中,然后G会放入全局队列中。
M0 和 G0
M0是启动程序后的编号为0的主线 程,这个M对应的实例例会在全局变量量runtime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G,在之后M0就和其他的M⼀样了。
G0是每次启动一个M都会第一个创建的gourtine,G0是仅用于负责调度的G,G0不指向任何可执⾏的函数, 每个M都会有一个⾃⼰的G0。在调度或系统调用时会使用G0的栈空间, 全局变量的G0是M0的G0。
trace 编程
Web查看方式
| |
Debug查看

SCHED 调试的信息
0ms 从程序启动到输出经历的时间
gomaxprocs P的数量 ⼀般默认是和CPU的核⼼心数是⼀致的
idleprocs 处理理idle状态的P的数量,gomaxprocs-idleprocs= ⽬前正在执行的p的数量
threads 线程数量(包括M0,包括GODEBUG调试的线程)
spinningthreads 处于
自旋状态的thread数量(自旋状态就是找到不G的M)
idlethread 处理idle状态的thread
runqueue 全局G队列中的G的数量
[0,0] 每个P的local queue本地队列列中,⽬目前存在G的数量量
Go调度器器GMP调度场景的全过程分析
P拥有G1,M1获取P后开始运行G1,G1使⽤用go func()创建了了G2,为了局部性G2优先加入到P1的本地队列。
G1运行完成后(函数:goexit),M上运行的goroutine切换为G0,G0负责调度时协程的切换(函数:schedule)。 从P的本地队列取G2,从G0切换到G2,并开始运行G2(函数:execute)。实现了线程M1的复用。



这三个场景是创建G后如何存放

规定:在创建G时,运行的G会尝试唤醒其他空闲的P和M组合去执⾏。
假定G2唤醒了M2,M2绑定了P2,并运行G0,但P2本地队列没有G,M2此时为⾃旋线程(没有G但为运行状态的线程,不断寻找G)。

M2尝试从全局队列(简称“GQ”)取一批G放到P2的本地队列(函数:findrunnable())。M2从全局队列取的G数量符合下⾯面的公式:
n = min(len(GQ)/GOMAXPROCS + 1, cap(GQ)/2)

全局队列已经没有G,那M就要执行work stealing(偷取):从其他有G的P那里偷取⼀半G过来,放到⾃己的P本地队列。P2从P1的本地队列尾部取一半的G,本例中⼀半则只有1个G8,放到P2的本地队列并执行。

最多有GOMAXPROCS个自旋的线程(当前例子中的GOMAXPROCS=4,所以一共4个P),多余的没事做的线程会让他们休眠。
假定当前除了M3和M4为自旋线程,还有M5和M6为空闲的线程(没有得到P的绑定,注意我们这里最多就只能够存在4个P,所以P的数量应该永远是M>=P, 大部分都是M在抢占需要运行的P),G8创建了G9, G8进行了阻塞的系统调用,M2和P2立即解绑,P2会执行以下判断:如果P2本地队列列有G、全局队列有G 或有空闲的M,P2都会⽴马唤醒1个M和它绑定,否则P2则会加⼊到空闲P列表,等待M来获取可用的P。 本场景中,P2本地队列列有G9,可以和其他空闲的线程M5绑定。
M2和P2会解绑,但M2会记住P2,然后G8和M2进⼊入系统调用状态。当G8和M2退出系统调用时,会尝试获取P2,如果无法获取,则获取空闲的P,如果依然没有,G8会被记为可运行状态,并加入到全局队列,M2因为没有P的绑定而变成休眠状态(长时间休眠等待GC回收销毁)。
疑问
- 当M绑定P的本地队列中的G为空时,是先从全局队列中获取P还是从其他P的队列中偷取?
| |
- 怎么理解
全局变量的G0是M0的G0?
| |
- GO调度完后放到哪里?
| |