深入 Go 底层原理(四):GMP 模型深度解析
1. 引言
在上一篇文章中,我们宏观地了解了 Go 的调度策略。现在,我们将深入到构成这个调度系统的三大核心组件:G、M、P。理解 GMP 模型是彻底搞懂 Go 并发调度原理的关键。
本文将详细解析 G、M、P 各自的职责以及它们之间是如何协同工作的。
2. GMP 核心组件
G (Goroutine):
定义:G 就是我们常说的 Goroutine。它是一个待执行的任务单元,包含了执行所需的栈空间、指令指针 (PC) 以及其他状态信息(如
goroutine status
)。特点:G 是轻量级的,初始栈大小仅为 2KB,可以根据需要动态伸缩。Go 程序可以轻松创建成千上万个 G。
状态:G 有多种状态,如
_Gidle
(闲置),_Grunnable
(可运行),_Grunning
(运行中),_Gsyscall
(系统调用中),_Gwaiting
(等待中),_Gdead
(已死亡) 等。调度器根据这些状态来移动 G。
M (Machine):
定义:M 是内核线程的抽象,是真正执行代码的实体。
runtime
会限制 M 的数量,默认不超过 10000。职责:M 从一个关联的 P 的本地队列中获取 G,然后执行 G 的代码。如果 G 发生系统调用或阻塞,M 可能会与 P 解绑。
M0
与M
:M0
是主线程,是程序启动时创建的第一个内核线程。
P (Processor):
定义:P 是处理器的抽象,它代表了 M 执行 Go 代码所需的上下文和资源。P 的数量在程序启动时被设置为
GOMAXPROCS
的值,通常等于 CPU 的核心数。职责:P 是 G 和 M 之间的“中间人”。它维护一个本地可运行 G 队列 (LRQ),为 M 提供可执行的 G。P 的存在使得调度器可以控制并发的程度(即同时有多少个 G 在真正地运行)。
关键作用:P 的引入实现了 M 和 G 的解耦。当一个 M 因为其上运行的 G 进行系统调用而阻塞时,P 会从这个 M 上解绑,并去寻找一个空闲的 M(或创建一个新的 M)来继续执行自己队列中的其他 G。这使得 Go 的并发能力不会因为部分 Goroutine 的阻塞而受限。
3. GMP 的协作流程
一个典型的调度场景如下:
启动:程序启动,创建 M0,并创建
GOMAXPROCS
个 P。G 的创建:
go func()
创建一个新的 G。这个新的 G 会被优先放入当前 M 绑定的 P 的本地队列 (LRQ) 的队头。M 的执行循环:
M 启动后,会绑定一个 P(
acquirep
)。M 进入一个无限的调度循环 (
schedule
)。在循环中,M 尝试从 P 的 LRQ、全局队列 (GRQ) 或通过工作窃取 (
runqsteal
) 来获取一个可运行的 G。找到 G 后,M 会执行 G 的代码 (
execute
)。G 执行完毕后,M 会再次进入调度循环寻找下一个 G。
系统调用场景 (Syscall):
假设 M0 上的 G0 准备进行一个会阻塞的系统调用。
M0 会与它的 P0 解绑。
P0 会去寻找一个空闲的 M(比如 M1)或者创建一个新的 M1。
P0 会绑定到 M1 上,并继续执行 P0 本地队列中的其他 G。
与此同时,原来的 M0 则陷入系统调用,等待其完成。
当 G0 的系统调用结束后,它会尝试获取一个空闲的 P 来继续执行。如果获取不到,G0 会被放入全局队列。M0 则会进入休眠。
这个设计使得 P(并发的许可证)不会因为 M 的阻塞而被浪费,从而保证了 GOMAXPROCS
个 Goroutine 可以持续地、并行地运行。
4. sysmon
监控线程
除了 G, M, P,还有一个重要的后台角色:sysmon
。它是一个由 runtime
启动的、不需要 P 的 M,在后台执行以下任务:
抢占:如上篇所述,向运行时间过长的 G 发送抢占信号。
网络轮询 (netpoller):检查网络 I/O 是否就绪,并唤醒因此阻塞的 G。
GC 辅助:在需要时触发 GC。
释放闲置资源:定期将长时间空闲的 M 和 P 的资源回收。
5. 总结
GMP 模型是 Go 语言高性能并发调度的核心。
G 是任务单元。
M 是执行实体(内核线程)。
P 是调度上下文和资源的提供者,是实现 M:N 模型的关键。
它们通过工作窃取、系统调用处理、异步抢占等机制紧密协作,构成了一个强大、高效、自适应的调度系统。