Skip to content

GMP 调度器

Go 的调度器是 M:N 调度器,多个 goroutine(G)映射到多个 OS 线程(M),通过逻辑处理器(P)调度。GMP 调度器实现了高效的并发和并行。

GMP 模型

G(Goroutine)

G 是 goroutine,Go 的用户态协程。G 包含:栈(初始 2KB)、程序计数器(当前执行的指令)、状态(运行、就绪、等待)。G 的状态切换:创建(_Gidle)、就绪(_Grunnable)、运行(_Grunning)、等待(_Gwaiting)、死亡(_Gdead)。

M(Machine)

M 是 machine,Go 对 OS 线程的抽象。M 包含:执行栈、G(当前运行的 G)、P(关联的 P)。M 可以阻塞(如系统调用),阻塞时 M 与 P 解绑,P 可以关联其他 M。

P(Processor)

P 是 processor,Go 的逻辑处理器。P 包含:本地 G 队列(runq,存储待运行的 G)、内存分配器(mcache)、GC 状态。P 的数量默认是 CPU 核心数,可以通过 GOMAXPROCS 调整。

调度流程

休眠状态

M0 是主线程(程序启动时创建),G0 是每个 M 的调度栈(不是用户 goroutine),P0 是主 P。初始时,P0 的本地队列为空,M0 与 P0 关联,M0 运行 G0(调度器代码)。

创建 G

创建 goroutine:go func() { ... }()。Go 运行时创建 G,G 的初始状态是 _Grunnable(就绪)。G 被添加到当前 P 的本地队列。如果本地队列满了(256 个),G 被添加到全局队列。

调度 G

M 从 P 的本地队列获取 G,如果本地队列为空,从全局队列获取 G(一次取多个)。如果全局队列也为空,从其他 P 的本地队列偷取 G(work stealing)。M 运行 G,G 的状态变为 _Grunning。

系统调用

G 执行系统调用(如文件 I/O),M 阻塞。M 与 P 解绑,P 可以关联其他 M(如果有空闲 M)。系统调用返回后,M 重新获取 P(可能是原来的 P,也可能是其他 P),继续运行 G。

如果系统调用时间过长(超过 20us),M 会被挂起,P 运行其他 G。系统调用返回后,M 重新关联 P,继续运行 G。

抢占式调度

Go 1.14 引入基于信号的抢占式调度。Go 运行时定期发送 SIGURG 信号给 M,M 收到信号后检查当前 G 是否执行时间过长,如果是,则挂起 G,调度其他 G。

抢占式调度防止某个 G 长时间占用 M,导致其他 G 饥饿。抢占式调度只针对用户代码,系统调用(如网络 I/O)本身就是可阻塞的。

工作窃取

本地队列优先

M 优先从关联的 P 的本地队列获取 G。本地队列的好处:不需要锁(单 M 访问)、CPU 缓存友好(数据局部性)。本地队列的大小是 256,超过则添加到全局队列。

全局队列

如果本地队列为空,M 从全局队列获取 G。获取 G 的数量:min(len(global), 61),一次获取多个 G,减少锁竞争。

工作窃取

如果本地队列和全局队列都为空,M 从其他 P 的本地队列偷取 G。偷取策略:从其他 P 的队列尾部偷取一半 G,避免频繁窃取。工作窃取保证了负载均衡,某个 P 的 G 很多,其他 P 可以窃取。

系统监控

Sysmon

Sysmon 是系统监控器,独立运行的 goroutine,定期检查:网络轮询器(netpoll)是否有就绪的 G、强制 GC(如果超过 2 分钟未 GC)、抢占长时间运行的 G(Go 1.13 之前)。

网络轮询器

网络轮询器监控网络文件描述符,当描述符就绪时,将关联的 G 放回全局队列。网络 I/O 不会阻塞 M,M 可以运行其他 G。网络轮询器基于 epoll/kqueue,高效监控大量描述符。

调度器优化

减少锁竞争

本地队列避免了全局队列的锁竞争。工作窃取时,只锁定被窃取的 P 的队列,不锁定所有 P 的队列。

减少上下文切换

Goroutine 切换只保存三个寄存器(SP、PC、DX),开销小(约 10-100ns)。线程切换需要保存所有寄存器,开销大(约 1-10us)。

充分利用多核

P 的数量默认是 CPU 核心数,每个 P 关联一个 M,充分利用多核。可以通过 GOMAXPROCS 调整 P 的数量,但不建议超过 CPU 核心数(会导致过多的上下文切换)。

常见问题

Goroutine 泄漏

Goroutine 泄漏是指 goroutine 无法退出,一直占用资源。常见原因:channel 阻塞(发送或接收无缓冲 channel)、死锁、无限循环。

避免 goroutine 泄漏:使用 context 取消 goroutine、使用 select + default 非阻塞发送、设置超时。

过多的 Goroutine

创建过多的 goroutine 会导致内存占用高(每个 goroutine 2KB)、调度开销大(工作窃取频繁)。解决方案:限制并发数(worker pool 模式)、使用缓冲 channel。

阻塞系统调用

阻塞系统调用(如文件 I/O)会阻塞 M,导致 P 无法运行其他 G。解决方案:使用 netpoller(网络 I/O)、使用非阻塞 I/O(O_NONBLOCK)。

GMP 调度器是 Go 高并发的核心,理解 GMP 的工作原理,有助于编写高效的 Go 程序,诊断性能问题。