「第16期」 距离大叔的80期小目标还有64期, 今天大叔要跟大家分享基础知识点是Golang的调度模型 —— GMP模型,面试过Golang岗位的小伙伴都知道这个知识点的分量有多重,基本是面必问,答必会的程度。下面一起看看吧。
什么是GMP调度模型?相信熟悉Golang的小伙伴对这个知识点并不陌生, 在面试遇到相关提问时,总能很轻松地回答出GMP的基本调度原理。
但是你有没有发现,面试官接着就很喜欢进行二次提问。比如:P的最大数量是多少?M的默认数量是多少?阻塞调度时是如何表现的?
如何更加全面地回答这个问题,确保不给面试官二次提问机会,这是本篇文章主要分享的内容。
接下来,请自助用餐。
GMP模型
GMP 模型是 Golang 中的协程调度模型,它将程序中的协程(Goroutine)分配给可用的处理器(Processor)和机器(Machine)来执行,以实现高效的并发操作。
GMP基本认识
-
G: Goroutine,每个 Goroutine 对应一个 G 结构体,G 存储 Goroutine 的运行堆栈、状态以及任务函数。
理论上G没有数量上限限制的,查看当前G的数量可以使用runtime. NumGoroutine() -
M: Machine,可以理解为 OS 线程,M只有在绑定有效的 P 后,才能执行其本地队列上的G,
M 并不保留 G 状态,这是 G 可以跨 M 调度的基础,M 默认也是最大的数量为10000个。 -
P: Processor,表示逻辑处理器, 对 G 来说,P 相当于 CPU 的角色,G 只有绑定到 P 的本地队列才能被调度。对 M 来说,P 提供了执行的上下文环境,P 的数量由启动时环境变量
$GOMAXPROCS或者是由runtime.GOMAXPROCS()决定。最大数量为 256个。 -
本地队列:每个 P 都有自己本地的队列,用于存放可执行的 G,是一个单向链表
-
全局队列:也是存放G的队列,全局队列是一个双向链表。G被放入到全局队列的情况包括:
-
- 当创建一个新的 G 时,如果当前 P 的本地队列已满(超过阀值),则将该 G 放入全局队列中。
- 当一个 G 被阻塞时,将其从当前 P 的本地队列中删除,并将其放入全局队列中。
- 当一个 G 在执行过程中被抢占时,将其从当前 P 的本地队列中删除,并将其放入全局队列中。
GMP调度流程
基本流程
-
当一个G被创建,将 G 加入到 P 的本地队列中,当 P 的本地队列中的 G 数量达到了一定阈值,为了防止 P 中的 G 长时间得不到执行,会将部分 G 从本地队列中转移到全局队列中,以便更好地利用 CPU 资源;
-
G 的运行需要 M 绑定一个P,接着 M 会启动一个 OS 线程,循环从 P 的本地队列里取出一个 G 并执行;
-
当 M 执行完当前 P 的本地队列里所有的G后,P 会利用 work-stealing 调度算法从全局队列或者其他 P 的本地队列中偷取可执行的 G 放入自己的队列中执行。
work-stealing 算法
具体来说,当一个P的本地队列中的任务执行完毕,它会先尝试从全局队列中获取任务。如果全局队列为空,则该P会随机选择一个其他的P,从其本地队列中获取一半的任务,加入自己的本地队列中,然后继续执行这些任务。这个过程称为work-stealing,因为该P从其他P那里"窃取"了一些任务。
在GPM模型中,work-stealing算法的使用可以提高系统的并发性和负载均衡,避免了某些P处于空闲状态,从而提高了系统的吞吐量。
阻塞调度
G在执行过程中,会发生两类的阻塞,一种是用户态阻塞,一种是系统调用阻塞
用户态阻塞/唤醒
当 goroutine 因为 channel 操作或者 network I/O 而阻塞时,有以下表现:
- 阻塞的 G 的状态由 _Gruning 变为 _Gwaitting,同时可能会被放置到某个 wait 队列(如 channel 的 waitq),
- G被抢占式调度,M 会跳过该 G 尝试获取并执行下一个 G,如果此时没有 runnable 的 G 供 M 运行,那么 M 将解绑 P,并进入 sleep 状态;
- 如果是因 channel 阻塞,当阻塞的 G 被另一端的 G2 唤醒时(比如 channel 的可读/写通知),G 被标记为 runnable,尝试加入 G2 所在 P 的 runnext,然后再是 P 的 Local 队列和 Global 队列。
- 如果是因 network I/O阻塞,G 的唤醒是通过事件的方式。操作系统会在数据准备好时生成一个 I/O 事件,操作系统通过事件通知机制(如 epoll)通知 Go 运行时系统中的 I/O 事件,Go 运行时系统使用 I/O 多路复用器(如
netpoll)来监听这些事件,并在事件发生时将对应的 goroutine 从阻塞状态中唤醒。
系统调用阻塞
G 被阻塞在某个系统调用上时,此时 G 会阻塞在 _Gsyscall 状态,M 也处于 block on syscall 状态,此时的 M 可被抢占调度:
- 执行该 G 的 M 会与 P 解绑,而 P 则尝试与其它 空闲的 M 绑定,继续执行其它 G。
- 如果没有其它 空闲的 的 M,但 P 的 Local 队列中仍然有 G 需要执行,则创建一个新的 M;
- 当系统调用完成后,
-
- G 会重新尝试获取一个 空闲的 P 进入它的 Local 队列恢复执行,如果没有 空闲的 P,G 会被标记为 runnable 加入到 Global 队列。
- 之前的 P 如果已经被其他 M 绑定, 则 M 会进入休眠状态,否则 M 继续绑定之前的 P 运行
总结
至此,相信小伙伴们对GMP调度模型会有一个更加清晰的认知,所以下次面试遇到GMP调度模型相关的提问,你可以这样说:
- GMP是Go的调度模型,G 是Go中的 Goroutine, M 可以理解为 OS 线程,最大数量为10000,P 相当于 CPU的角色,最大数量为256,也可以由
runtime.GOMAXPROCS()决定。 - 一个G被创建出来,它会被放到 P 的本地队列中,当 P 的本地队列到达一定阀值后,新创建的 G 会被存放到全局变量中。G的调度执行需要 P 绑定一个 M
- 当 G 发生网络I/O或者channel操作阻塞时,G会被抢占式调度,被阻塞的 G 的状态由 _Gruning 变为 _Gwaitting,会被放入 waiting 队列或者全局队列中。M 会跳过该 G 尝试获取并执行下一个 G,如果此时没有 runnable 的 G 供 M 运行,那么 M 将解绑 P,并进入 sleep 状态;
- 当 G 被发生系统调用时,此时 G 会阻塞在 _Gsyscall 状态,M 也处于 block on syscall 状态,此时的 M 可被抢占调,执行该 G 的 M 会与 P 解绑,而 P 则尝试与其它 空闲的 M 绑定,继续执行其它 G。如果没有其它 空闲的 的 M,但 P 的 Local 队列中仍然有 G 需要执行,则创建一个新的 M。