golang GMP

发布时间 2023-11-17 15:01:32作者: _小孟同学

定义

G:Goroutine的缩写,一个G代表了对一段需要被执行的Go语言代码的封装
M:Machine的缩写,一个M代表了一个内核线程
P:Processor的缩写,一个P代表了M所需的上下文环境

定义都在源码 runtime/runtime2.go

M

M 的定义在runtime/runtime2.go
一个 M 代表了一个内核线程,用以执行 P
M 的数量限制是 10000 个

P

P 是指上下文,同时也代表最多同时有多少个G在运行
里面保存了一个本地队列 runq [256]guintptr
P有几个状态

  • _Pidle: 空闲状态,目前没有任务
  • _Prunning: 运行状态,代表 P获取到了 M 并正在执行
  • _Psyscall: P 正在运行内核代码
  • _Pgcstop: 因为 gc 导致停止
  • _Pdead: 死亡 P,可能GOMAXPROCS增加的话重新启动

G

G就是 goroutine ,通过 go func()就可以启动一个新的 groutine
G 的知识点很多,后面再补充

schedt

还有一个schedt 保存全局的变量,包括全局 goroutine 队列等,用一个 int32 表示全局 goroutine 队列长度

启动

启动流程

  • runtime创建最初的线程m0和goroutine g0,并把2者关联。
  • 调度器初始化:初始化m0、栈、垃圾回收,以及创建和初始化由GOMAXPROCS个P构成的P列表。
  • 示例代码中的main函数是main.main,runtime中也有1个main函数——runtime.main,代码经过编译后,runtime.main会调用main.main,程序启动时- - 会为runtime.main创建goroutine,称它为main goroutine吧,然后把main goroutine加入到P的本地队列。
  • 启动m0,m0已经绑定了P,会从P的本地队列获取G,获取到main goroutine。
  • G拥有栈,M根据G中的栈信息和调度信息设置运行环境
  • M运行G
  • G退出,再次回到M获取可运行的G,这样重复下去,直到main.main退出,runtime.main执行Defer和Panic处理,或调用runtime.exit退出程序。

调度

源码在 runtime/proc.go

一个 M1 挂载 P1 正在运行 G1,此时 G1 启动一个 G2 ,这时会把G2 放到 G1 运行的 P1 的本地队列,等待下次运行
但是当 P1 的本地队列满了(256 个)情况下,会将 G2 和本地队列的一般 G 打乱全放到全局队列,等待运行

当一个 M2 绑定的 P2 里面的本地队列为空的情况下,此时 M2 进入自旋状态(自旋状态,表示 M 绑定了 P 又没有获取 G,最多有 GOMAXPROCS 个自旋线程),此时 M2 会获取可运行的队列,先判断是否在 gc,然后从全局队列获取可运行的 G(获取全局队列的 min(len(gq)/GOMAXPROCS + 1,len(gq/2) 个),没有的话查看网络 poll, 如果都没有就从其他 P 窃取,窃取的规则是随机的一个 P 的一半

当一个 M3 绑定的 G3 发生了阻塞调用或系统调用,会将 M3 和 G3 与 P剥离,P 寻找一个新的 M 执行剩下的 G,等 G3 执行完以后会尝试挂载 P,成功就继续运行,失败就将 M3 休眠,把 G3 放入全局队列

参考

Scalable Go Scheduler Design Doc
2、Golang的协程调度器原理及GMP设计思想?
Go的GMP调度器工作原理及源码解析(一)
Golang并发模型GMP