GMP是Go语言运行时的一种调度模型,用于管理Goroutines、内存和处理器资源。
一、G、M、P分别指的什么?
1.1 G(goroutine):Goroutine是Go语言中的轻量级现成。
- goroutine的新建, 休眠, 恢复, 停止都受到go运行时的管理
- goroutine执行异步操作时会进入休眠状态, 待操作完成后再恢复, 无需占用系统线程
- goroutine新建或恢复时会添加到运行队列, 等待M取出并运行
1.2 M(machine):代表操作系统系统的线程。每个M是一个实际的操作系统线程
- M会从P(本地队列)中取出G,然后运行G,如果G运行完毕或进入休眠状态,则从运行队列中取出下一个G运行,周而复始。
- 有时G需要调用一些无法避免阻塞的原生代码,这时M会继续保持阻塞状态 等待G的执行,但是P会被释放。P被释放后被其他空闲M继续调用
- go代码, 即goroutine, M运行go代码需要一个P
- 原生代码, 例如阻塞的syscall, M运行原生代码不需要P
1.3 P(process):代表处理器。是Go运行时系统重的一个逻辑处理单元,包含了任务队列和与执行goroutine相关的资源。
- P的数量通常由
GOMAXPROCS
指定,默认值为运行环境中的CPU核数。 - 如果P的数量等于1,代表当前最多只能有一个线程(M)执行go代码
- 如果P的数量等于2,代表当前最多只能有两个线程(M)执行go代码
二、调度模型
2.1 Goroutine创建与分配:
- 当你创建一个新的Goroutine时(例如
go task()
),这个Goroutine被放置在某个P处理器的任务队列中。 - 如果所有P的任务队列都满了,新的Goroutine会被放置到全局队列中。
2.2 M线程获取P处理器:
- 每个M线程都会获取一个P处理器,并开始执行这个P处理器任务队列中的Goroutine。
- 一个M线程一次只能与一个P处理器绑定,并执行这个P中的一个Goroutine。
2.3 执行与切换:
- M线程运行P处理器的任务队列中的Goroutine,直到这个Goroutine完成或被阻塞(例如等待I/O)。
- 如果一个Goroutine被阻塞,M线程会将其挂起,并尝试从P处理器的任务队列中获取下一个可运行的Goroutine。
- P处理器的任务队列为空时,M线程会尝试从全局队列中获取任务或从其他P处理器的任务队列中偷取任务。
2.4 任务抢占:
- Go调度器会定期检查是否需要切换Goroutine,以确保所有Goroutine都有机会运行。
- 如果一个Goroutine运行时间过长,它可能会被抢占,以便其他Goroutine有机会运行。
2.5 总结:
-
Goroutine是轻量级的协程,被放置在P处理器中。
-
M是操作系统的线程,一个M线程一次只能执行一个P处理器中的一个Goroutine。
-
调度器负责在多个M线程和P处理器之间公平地分配Goroutine,以实现高效的并发执行。
三、数据结构
3.1 G的状态
- 空闲中:表示G刚创建,仍未初始化
- 待运行:G刚在运行队列中,等待M取出运行
- 运行中:M正在运行G,此时M会拥有一个P
- 系统调用中:M正在运行这个G发起的系统调用,这时候M并不拥有P
- 等待中:G在等待某些条件完成,这时G不在运行也不在运行队列中
- 已中止:G未被使用,可能已执行完毕
- 栈复制中:G正在获取一个新的栈空间并把原来的内容复制过去
3.2 M的状态
-
自旋中:M正在从运行队列中获取G,这时M会拥有一个P
-
执行go代码中: M正在执行go代码, 这时候M会拥有一个P
-
执行原生代码中: M正在执行原生代码或者阻塞的syscall, 这时M并不拥有P
-
休眠中: M发现无待运行的G时会进入休眠, 并添加到空闲M链表中, 这时M并不拥有P
3.3 P的状态
- 空闲中:当M发现无待运行的G时会进入休眠, 这时M拥有的P会变为空闲并加到空闲P链表中
- 运行中:当M拥有了一个P后, 这个P的状态就会变为运行中, M运行G会使用这个P中的资源
- 系统调用中:当go调用原生代码, 原生代码又反过来调用go代码时, 使用的P会变为此状态
- GC停止中:当gc停止了整个世界(STW)时, P会变为此状态
- 已中止:当P的数量在运行时改变, 且数量减少时多余的P会变为此状态
3.4 本地运行队列
-
go中有多个运行队列可以保持待运行的G。分别是各个P中的本地运行队列和全局运行队列。
-
入队待运行的G时会优先加到当前P的本地运行队列, M获取待运行的G时也会优先从拥有的P的本地运行队列获取。
-
本地运行队列入队和出队不需要使用线程锁。
-
本地运行队列有数量限制, 当数量达到256个时会入队到全局运行队列。
-
本地运行队列的数据结构是环形队列, 由一个256长度的数组和两个序号(head, tail)组成。
3.5 全局运行队列
-
全局运行队列保存在全局变量
sched
中, 全局运行队列入队和出队需要使用线程锁。 -
全局运行队列的数据结构是链表, 由两个指针(head, tail)组成。
3.6 空闲M链表
当M发现无待运行的G时会进入休眠,并添加到空闲M链表中,空闲M链表保存在全局变量sched
。
进入休眠的M会等待一个信号量(m.park),唤醒休眠的M会使用这个信号量。
go需要保证有足够的M运行G,实现机制是:
- 入队待运行的G后, 如果当前无自旋的M但是有空闲的P, 就唤醒或者新建一个M3
- 当M离开自旋状态并准备运行出队的G时, 如果当前无自旋的M但是有空闲的P, 就唤醒或者新建一个M
- 当M离开自旋状态并准备休眠时, 会在离开自旋状态后再次检查所有运行队列, 如果有待运行的G则重新进入自旋状态
"入队待运行的G"和"M离开自旋状态"会同时进行, go会使用这样的检查顺序:
入队待运行的G => 内存屏障 => 检查当前自旋的M数量 => 唤醒或者新建一个M
减少当前自旋的M数量 => 内存屏障 => 检查所有运行队列是否有待运行的G => 休眠
3.7 空闲P链表
当P的本地运行队列中的所有G都运行完毕, 又不能从其他地方拿到G时,拥有P的M会释放P并进入休眠状态, 释放的P会变为空闲状态并加到空闲P链表中, 空闲P链表保存在全局变量sched
。
下次待运行的G入队时如果发现有空闲的P, 但是又没有自旋中的M时会唤醒或者新建一个M, M会拥有这个P, P会重新变为运行中的状态。