go语言中的GMP模型和GM模型浅析介绍

2023-06-01 00:00:00 语言 模型 浅析

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的绑定而变成休眠状态

相关文章