Golang里面的GMP调度模型到底是个啥?今天,交互动画+现实人物模型明明白白地告诉你!

Updated on with 0 views and 0 comments

本文基于go1.14.12版本

GMP模型文字定义

G:goroutine,可以理解成一个计算任务。由需要执行的代码和其上下文组成,上下文包括:当前代码位置,栈顶、栈底地址,状态等。

M:machine,系统线程,执行实体,想要在CPU上执行代码,必须有线程,与C语言中的线程相同,通过系统调用clone来创建。

P:processor,虚拟处理器,M必须获得P才能执行代码,否则必须陷入休眠(后台监控程序出外),你也可以将其理解为一种 token ,有了这个token,才有在物 CPU 核心上执行的权利。

G的数量一般比M要多很多,所以有时也有人称其为 M:N 模型

go 的调度本质上是一个生产-消费流程:

生产流程:

image.png

goroutine 创建流程:

  • runnext 为空:
    • 把 g 放入 runnext,直接返回
  • runnext 不为空:
    • 通过自旋锁把 g 放入 runnext
    • 放置被 kick out 的goroutine
      • P 的本地队列未满(256)
        • 放入本地队列的队尾,本地队列是个数组
      • P 的本地队列已满 (256)
        • 拿出本地队列的一半出来,组建临时链表,将当前 gp 放入链表末尾
        • 将临时链表挂在全局队列的末尾,全局队列是个链表

这里我们举个例子:

以下代码会输出什么呢?

package main

import "fmt"
import "runtime"

func main() {
	runtime.GOMAXPROCS(1)
	for i := 0; i < 10; i++ {
		i := i
		go func() {
			fmt.Println("A: ", i)
		}()
	}

	var ch = make(chan int)
	<-ch
}

输出内容为:

A:9
A:0
A:1
A:2
A:3
A:4
A:5
A:6
A:7
A:8
  • 首先,在1个操作系统线程的情况下,只有main携程执行遇到阻塞等待时,其子携程才能被执行,而子携程在执行顺序上正好对应于它们入队的顺序。

  • 那为什么,最后一个9总是在第一个呢?

  • 其中 Go 1.13.8 和 Go1.14.6 在实现上和早期版本有一点不同,每增加一个子携程就把其对应函数数地址存放到 “runnext” , 而把 “runnext” 上原来的地址 (即上一个入队的子携程对应的函数地址) 移动到本地队列中去了,这样当执行到 ←ch 阻塞的时候,“runnext” 存放的就是最后一个入队的函数地址了。

  • 当开始执行队列中函数的时候,会首先执行 “runnext” 上的函数,然后再按照先进先出的顺序,执行本地队列中的函数。所以就解释了为什么 A: 9 总在第一个,而后面则是队列顺序打印的问题。

    Goroutine生产过程交互动画 - 点我开始互动演示

消费流程:

消费过程本质上就是 runtime.schedule 函数起执行流程如下:
image.png

schedule调度过程:

  • 为了保证公平性,每调度 61 次 (g.m.p.ptr().scheditick % 61 == 0) 并且全局队列中还有待处理的 goroutine,就从全局队列获取队首的 goroutine 处理。

  • 普通情况下,就是从本地队列中获取 goroutine

    • 第一步:首先查看 runnext 上是否有 routine ,如果有则直接拿来处理,schedule return
    • 第一步:首先查看 runnext 上是否有 routine ,如果有则直接拿来处理,schedule return
    • 第三步:如果全局队列有待处理的 goroutine
      • 从全局 goroutine 队列中 获取 total/gomaxprocs +1 个,但是不能超过128个goroutine
      • 取队首的goroutine处理,剩余的goroutine按队列顺序放入本地队列中
    • 第四步:如果runnext、本地队列、全局队列都没有goroutine了
      • 则从其他的 P 的本地队列中从队首取一半的goroutine
      • 前面的n-1个放入本地队列
      • 最后一个拿去给M处理

    Goroutine消费过程交互动画 - 点我开始互动演示

现实模型

为了加深理解,我们可以借助现实中关系模型来模拟GMP模型:

为了方便理解,我们来套用现实中的调度模型:

我们假设这套调度流程是发生在我们公司内部的,你是一个技术员,公司里有很多和你一样的技术员,公司给每个技术员配了两个助手,那么我们先定义一下身份:

  • G 就是公司当前要做的一个任务
  • M 就是你自己
  • P 就是公司每个员工的电脑,是你完成工作的主要工具。
  • runnext 就是你的任务收集助手,你的任务收集助手会帮你接收新的任务,并帮你把工作放入备忘录,
  • runq 就是你的备忘录,你的任务都会按照先后顺序写入备忘录(即本地队列),容量为256个任务
  • globrunq 公司的任务池(即全局队列),容量无限
  • runtime.schedule 就是你的任务提取助手,他会帮你取你接下来要执行的任务
生产过程:

公司的产品经理或者你的上司会不定时的塞给你一些任务(G)让你(M)去完成。

  • 比如有一天,你的领导A想给你(M)分配一个任务T1(G)
    • 这时候你的任务收集助手(runnext)会拿起这个任务T1(G)
      • 如果你(M)此时空闲,任务提取助手(runtime.schedule)会先去问任务收集助手(runnext)要一个任务(G)来执行,任务收集助手(runnext)就会把手上的任务T1(G)交给任务提取助手(runtime.schedule),再传递给你(M)执行
  • 紧接着,公司的产品经理B给你(M)提了一个需求任务T2
    • 这个时候你的任务收集助手(runnext)会把未交给你(M)的任务T1放入你的备忘录(runq)
    • 而你的任务收集助手(runnext)会拿起任务T2(G)
      • 如果你(M)此时空闲,任务提取助手(runtime.schedule)会先去问任务收集助手(runnext)要一个任务(G)来执行,任务收集助手(runnext)就会把手上的任务T2(G)交给任务提取助手(runtime.schedule),再传递给你(M)执行
  • 又一次,领导C又带来一个任务T3(G)要交给你做
    • 任务收集助手(runnext)会把手头上你(M)没有拿走的T2(G)按照先后顺序放入备忘录(runq)T1(G)的后面
    • 任务收集助手(runnext)拿起任务T3(G)
  • 好多次之后,你的备忘录里面已经有256个了,已经放不下了,可又有人给你提任务T258(G),任务太多了,你找领导抱怨,领导说让你把你的任务分出一部分放到公司任务池吧,于是你就让任务收集助手(runnext)去清理了。
    • 任务收集助手(runnext)将备忘录里取出后面一半的任务(G)即 T129-T256,将手里的任务T257(G)放在T256(G)后面,然后先后顺序整体放到公司待完成的任务池(globrunq)里最后一个任务的后面。
    • 然后任务收集助手拿起T258
  • 又来一个任务T259(G)
    • 任务收集助手(runnext)把手里的T258(G)放入备忘录(runq)里面最后一个任务T128(G)的后面
    • 任务收集助手(runnext)再拿起T259(G)
消费过程:

你(M)开始要开始执行任务(G)了

  • 首先任务提取助手(runtime.schedule)会先去问问你的任务收集助手(rnunext),有没有什么任务(G)要做?
    • 任务收集助手(runnext)把手里的T259(G)拿给任务提取助手(runtime.schedule)再传递给你(M),然后你(M)开始执行T259(G)
  • T259(G)执行完毕,任务提取助手(runtime.schedule)又去问任务收集助手(runnext),任务收集助手(runnext)手里没有了,说你去备忘录看看吧。
    • 然后任务提取助手(runtime.schedule)就开始去备忘录看
    • (runq)备忘录有T1......T128-T258
    • 任务提取助手(runtime.schedule)按照先后顺序,拿走了T1(G)给你(M)执行
  • T1(执行完毕),任务提取助手(runtime.schedule)每次都会先去问任务收集助手(runnext),没有任务,则继续去备忘录(runq)着
    • (runq)备忘录有T2......T128-T258
    • 任务提取助手(runtime.schedule)按照先后顺序,拿走了T2(G)给你(M)执行
  • 执行完T60(G),任务提取助手(runtime.schedule)一看次数,呀!已经执行了61次了,公司任务池(globrunq)里的任务(G)好久没动了,不行,得公平点,去公司任务池(globrunq)拿一个任务(G)来执行吧。
    • 任务提取助手(runtime.schedule)从公司任务池(globrunq)里拿走了先后顺序中第一个任务T128(G)
  • 执行了好多,把T28(G)、T258(G)执行完之后,任务提取助手(runtime.schedule)又出动了
    • 先去问了任务收集助手(runnext),没有
    • 又去看了备忘录(runq),也没有了
    • 老板肯定不能让你闲着呀,于是,老板说你去公司任务池里拿一些任务
    • 然后就从公司任务池(globrunq)里拿出一部分任务(G)
      • 具体拿多少呢?
      • 拿 任务池(globrunq)(总量/像你这样的员工的数量 + 1 ),但是不能超过256个
      • 放入备忘录(runq)
      • 拿出第一个来执行
  • 最后公司任务池的任务都被执行完了,怎么办呢?老板说,你没看别的同时还在忙吗?去帮帮忙呀。
    • 于是任务提取助手(runtime.schedule)就跑到其他员工(M)那里,偷偷的从它备忘录的头部开始拿走它当前备忘录里任务(G)数量的一半。
    • 然后把拿走的最后一个给你(M)执行,前面的放入你自己的备忘录里(runq)

标题:Golang里面的GMP调度模型到底是个啥?今天,交互动画+现实人物模型明明白白地告诉你!
作者:wangmengke
地址:http://solo.fancydigital.com.cn/articles/2021/05/20/1621506233201.html