2023-11-12    2025-03-18    1939 字  4 分钟
Go

非原创

好文:https://yizhi.ren/2019/06/03/goscheduler

参考:https://blog.csdn.net/xmcy001122/article/details/119392934

Go 的 GMP 模型(Goroutine-M-Processor)是 Go 语言实现高并发的核心调度机制,它通过用户态的轻量级调度器管理 Goroutine,并高效复用操作系统的线程(Thread)


1. GMP 核心组件

GMP 模型由三个核心部分组成:

  • G(Goroutine):用户级的轻量级协程,存储执行栈、程序计数器(PC)和状态信息。
  • M(Machine):操作系统线程(OS Thread)的抽象,由操作系统直接调度,负责执行 G 的代码。
  • P(Processor):逻辑处理器,是 G 和 M 之间的协调者,每个 P 维护一个本地 Goroutine 队列(Local Queue)。

2. GMP 调度器的设计目标

  • 高并发:支持百万级 Goroutine 并发。
  • 低延迟:减少 Goroutine 切换和调度的开销。
  • 资源高效:避免频繁创建/销毁线程,复用系统资源。

3. GMP 调度机制详解

**(1) P(Processor)的作用

  • P 是 Go 调度器的核心,决定并发执行的并行度(通过 GOMAXPROCS 设置 P 的数量,默认等于 CPU 核数)。
  • 每个 P 维护一个本地运行队列(Local Queue),用于存放待执行的 Goroutine。
  • P 还负责管理一些关键资源(如网络轮询器、计时器等)。

(2) M(Machine)与线程绑定

  • M 是操作系统线程的抽象,必须绑定一个 P 才能执行 G。
  • 如果 M 在执行 G 时发生阻塞(如系统调用),会释放绑定的 P,让其他 M 接管 P 继续执行其他 G。

(3) Goroutine 的调度流程

  1. 创建 G:当启动一个 Goroutine(go func())时:

    • G 会被放入当前 P 的本地队列。
    • 如果本地队列已满,则放入全局队列(Global Queue)。
  2. M 获取 G

    • M 优先从绑定的 P 的本地队列获取 G。
    • 如果本地队列为空,M 会尝试从全局队列窃取 G。
    • 如果全局队列也为空,M 会从其他 P 的本地队列“偷取”(Work Stealing)一半的 G。
  3. 执行 G

    • M 执行 G 的代码,直到 G 主动让出 CPU(如遇到 channel 阻塞、time.Sleep 等)。
    • 当 G 阻塞时,M 会释放 P,进入休眠状态,等待被唤醒。
  4. 系统调用处理

    • 如果 G 执行了阻塞式系统调用(如文件 I/O),Go 调度器会将 M 和 G 分离,并创建新的 M 接管 P 继续执行其他 G。
    • 当系统调用返回,G 会被重新放入队列,M 则进入休眠或销毁。

4. 关键调度策略

(1) 工作窃取(Work Stealing)

  • 当 P 的本地队列为空时,会优先从全局队列获取 G,若全局队列也为空,则从其他 P 的本地队列窃取一半的 G。
  • 目的:平衡各 P 的工作负载,避免“饥饿”。

(2) 抢占式调度(Preemption)

  • Go 1.14 之前,Goroutine 只能通过函数调用(如 channel 操作)主动让出 CPU。
  • Go 1.14+ 引入了基于信号的抢占式调度,避免长时间运行的 Goroutine 阻塞其他任务。

(3) 自旋线程(Spinning Thread)

  • 当 M 找不到可执行的 G 时,会短暂进入“自旋”状态(不释放 CPU),等待新的 G 加入队列。
  • 目的:减少线程频繁休眠和唤醒的开销。

5. GMP 的运行示例

场景 1:Goroutine 正常执行

  1. 启动 4 个 P(假设 GOMAXPROCS=4),每个 P 绑定一个 M。
  2. 创建 1000 个 G,大部分进入各 P 的本地队列。
  3. 每个 M 从自己的 P 队列获取 G 并执行。

场景 2:Goroutine 阻塞

  1. 某个 G 执行 time.Sleep,主动让出 CPU。
  2. M 将 G 放入“等待队列”,并继续执行其他 G。

场景 3:系统调用阻塞

  1. 某个 G 执行阻塞式系统调用(如 http.Get)。
  2. M 会释放 P,由其他 M 接管 P 继续执行。
  3. 当系统调用完成,G 会被重新放入队列等待执行。

6. 为什么需要 P(Processor)?

  • 解耦 M 和 G:避免 M 直接管理 G,减少锁竞争。
  • 资源控制:通过 GOMAXPROCS 限制并行度,防止过度消耗 CPU。
  • 本地队列:每个 P 维护本地队列,减少全局队列的锁争用。

7. 可视化模型:餐厅比喻

  • G(顾客):需要被服务的请求。
  • M(服务员):实际处理请求的人。
  • P(餐台):服务员的工作台,存放待处理的订单(本地队列)。
    • 服务员(M)从自己的餐台(P)优先取订单(G)。
    • 如果自己的餐台空了,可以去其他餐台“偷”订单(Work Stealing)。

8. 总结:GMP 的优势

特性 说明
高并发 轻松支持百万级 Goroutine。
低开销 用户态调度,避免频繁的线程切换和锁竞争。
负载均衡 Work Stealing 机制自动平衡各 P 的任务。
高效系统调用 阻塞时自动释放 P,由其他 M 继续执行任务。
抢占式调度 防止单个 Goroutine 长时间占用 CPU。

9. 实际开发中的注意事项

  1. 设置 GOMAXPROCS:通常设为 CPU 核数,但 I/O 密集型任务可适当增加。
  2. 避免阻塞 Goroutine:减少长时间阻塞操作,改用异步 I/O 或 context 超时控制。
  3. 监控调度延迟:使用 go tool tracepprof 分析调度性能。

通过 GMP 模型,Go 在用户态实现了高效的协程调度,完美平衡了并发性能和资源消耗。