2021-10-18 16:41:25

GMP调度器

GMP调度器

主要内容来自 潘少 的 gnet开源说。 https://github.com/gocn/opentalk

G、M、P 是什么

image-20211018161626840

G: 表示 Goroutine,每个 Goroutine 对应一个 G 结构体,G 存储 Goroutine 的运行堆栈、状态以及任务函数,可重用。G 并非执行体,每个 G 需要绑 定到 P 才能被调度执行。

P: Processor,表示逻辑处理器, 对 G 来说,P 相当于 CPU 核,G 只有绑定到 P(在 P 的 local runq 中)才能被调度。对 M 来说,P 提供了相关的执行 环境(Context),如内存分配状态(mcache),任务队列(G)等,P 的数量决定了系统内最大可并行的 G 的数量(前提:物理 CPU 核数 >= P 的数量),P 的数量由用户设置的 GOMAXPROCS 决定,但是不论 GOMAXPROCS 设置为多大,P 的数量最大为 256。

M: Machine,OS 线程抽象,代表着真正执行计算的资源,在绑定有效的 P 后,进入 schedule 循环;而 schedule 循环的机制大致是从 Global 队列、P 的 Local 队列以及 wait 队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 goexit 做清理工作并回到 M,如此反复。M 并不保留 G 状态,这是 G 可以跨 M 调度的基础,M 的数量是不定的,由 Go Runtime 调整,为了防止创建过多 OS 线程导致系统调度不过来,目前默认最大限制为 10000个。

image-20211018161849361

GMP scheduler (从 M0 开始)会不断循环调用 runtime.schedule() 去调度 goroutines,而每个 goroutine 执行完成并退出之后,会再次调用 runtime.schedule(),使得调度器回到调度循环去 执行其他的 goroutine,不断循环,永不停歇。

runtime.schedule --> runtime.execute --> runtime.gogo --> goroutine code --> runtime.goexit --> runtime.goexit1 --> runtime.mcall --> runtime.goexit0 --> runtime.schedule

当我们使用 go 关键字启动一个新 goroutine 时, 最终会调用 runtime.newproc --> runtime.newproc1,来得到 g,runtime.newproc1 会先从 P 的 gfree 缓存链表中查找可用的 g,若缓 存未生效,则会新创建 g 给当前的业务函数,最后 这个 g 会被传给 runtime.gogo 去真正执行。

轮询goroutine的顺序

image-20211018162104278

当我们通过 go 关键字创建一个 goroutine 时,内部会调用 runtime.newproc --> runtime.newproc1 封存函数上下文信息得到 g;然后先尝试把这个待运行的 g 放 到 P 的本地队列里,如果本地队列已经满了则放到全局队列中;

GMP 在启动之后会进入一个调度循环,用一种相对公平的方式查找并执行 g:

  1. 首先每执行 61 次 g 之后会从全局队列拿一个 g 出来执行。
  2. 然后尝试从 P 本地队列里取 g。
  3. 如果这两个队列都为空,则调用 findrunable(),阻塞地取 g。

runtime.findrunnable() 函数的查找逻辑如下:

  1. 先从 P 本地队列里查找。
  2. 然后从全局队列里找。
  3. 如果这两个队列都是空的,则从网络轮询器里查找。
  4. 最后如果还是没找到 g,则去偷其他 P 的 g,优先尝试偷 timer。

总之,你可以认为 findrunnable() 肯定会返回一个可运行的 g 给调度器

阻塞调度

image-20211018162351776

系统调用阻塞

image-20211018162501256

当 G 被阻塞在某个系统调用上时,此时 G 会阻塞在 _Gsyscall 状态,M 也处于 block on syscall 状态,此时 的 M 可被(sysmon 线程)抢占调度:执行该 G 的 M 会与 P 解绑,而 P 则尝试与其它 idle 的 M 绑定,继续执行其 它 G。如果没有其它 idle 的 M,但 P 的 Local 队列中仍 然有 G 需要执行,则创建一个新的 M;当系统调用完成 后,G 会重新尝试获取一个 idle 的 P 进入它的 Local 队 列恢复执行,如果没有 idle 的 P,G 会被标记为 runnable 加入到 Global 队列。

用户态阻塞

当 goroutine 因为 channel 操作或者 network I/O 而阻塞 时(实际上 golang 已经用 netpoller 实现了 goroutine 网 络 I/O 阻塞不会导致 M 被阻塞,仅阻塞 G,这里仅仅是 举个栗子),对应的 G 会被放置到某个 wait 队列(如 channel 的 waitq 和 sendq),该 G 的状态由 _Gruning 变 为 _Gwaitting ,而 M 会跳过该 G 尝试获取并执行下一 个 G,如果此时没有 runnable 的 G 供 M 运行,那么 M 将解绑 P,并进入 sleep 状态;当阻塞的 G 被另一端的 G2 唤醒时(比如 channel 的可读/写通知),G 被标记为 runnable,尝试加入 G2 所在 P 的 runnext,然后再是 P 的 Local 队列和 Global 队列。

调度方式

image-20211018162729984

本文链接:http://blog.go2live.cn/post/gmp.html

-- EOF --