2. GMP 模型总览:Goroutine、Machine、Processor 三大核心组件及其关系

Go 语言的并发调度模型被称为 GMP 模型,它是 Go 运行时(runtime)实现高并发、轻量级调度的核心架构。理解 GMP 是掌握 Go 并发性能调优的基石。本章将逐一拆解三大核心组件——Goroutine (G)、Machine (M)、Processor (P),并阐明它们之间的协作关系。

2.1 三大核心组件定义

组件 全称 核心职责 数量级 / 约束
G Goroutine 轻量级用户态线程,承载用户任务(函数/闭包) 百万级;由 Go 运行时创建和销毁
M Machine (内核线程) 实际执行 Go 代码的底层操作系统线程 通常数千;与内核线程 1:1 对应
P Processor (逻辑处理器) 调度上下文,管理可运行的 G 队列,为 M 提供执行资源 默认等于 CPU 核心数;由 GOMAXPROCS 控制
关键区别: G 是任务载体,M 是执行者,P 是调度器。没有 P,M 无法执行任何 G。

2.2 组件内部结构速览

Goroutine (G)

  • 栈:初始 2KB,可动态增长(上限 1GB),远小于内核线程栈。
  • 状态:_Gidle、_Grunnable、_Grunning、_Gsyscall、_Gwaiting 等。
  • 调度信息:保存 PC、SP、BP 等寄存器上下文,用于恢复执行。
  • 关联 M:当前正在执行该 G 的 M(仅在 _Grunning 时有效)。

Machine (M)

  • 线程结构:对应一个操作系统线程,包含 TLS(线程本地存储)。
  • 当前 G:curg 指针,指向正在执行的 G。
  • 自旋状态:spinning 标志,表示 M 正在空闲寻找可运行 G。
  • 关联 P:p 指针,绑定一个 P 才能执行 G。

Processor (P)

  • 本地运行队列 (LRQ):每个 P 拥有一个长度为 256 的环形队列,存放 _Grunnable 状态的 G。
  • 全局运行队列 (GRQ):全局共享队列,当 LRQ 满时 G 放入 GRQ。
  • 空闲 G 列表:缓存已退出的 G,用于复用,减少内存分配。
  • 关联 M:当前绑定的 M,若为 nil 表示 P 空闲。

2.3 GMP 核心关系:调度循环

GMP 模型的核心调度逻辑可以概括为:M 绑定 P,P 从队列中获取 G,M 执行 G。当 G 发生阻塞(如系统调用、Channel 操作)或主动让出时,调度器会切换 G,实现高并发。

调度循环口诀: M 找 P,P 找 G,G 跑任务,阻塞换人。

2.4 GMP 关系架构图

下图展示了 GMP 三大组件之间的静态绑定与动态调度关系:

GMP 三大组件关系架构图 GMP 三大组件关系架构图 Goroutine 池 (G) G1 (运行中) G2 (可运行) G3 (阻塞) G4 (可运行) G5 (可运行) Processor (P) 本地运行队列 (LRQ) G2 G4 G5 空闲 G 缓存 (复用已退出 G) Machine (M) M1 (运行中) 绑定 P1,执行 G1 M2 (自旋) 空闲,寻找可运行 G M3 (阻塞) G3 系统调用中 M4 (空闲) 未绑定 P 执行 入队 绑定 阻塞 Goroutine Processor Machine 绑定/执行 阻塞

2.5 核心调度流程:M 如何获取 G 并执行

  1. M 绑定 P:M 必须与一个 P 绑定才能执行 Go 代码。空闲 M 会尝试从全局空闲 P 列表获取一个 P。
  2. 从 LRQ 获取 G:M 优先从绑定的 P 的本地运行队列(LRQ)头部获取一个 _Grunnable 状态的 G。
  3. 从 GRQ 获取 G:若 LRQ 为空,M 会从全局运行队列(GRQ)获取一批 G(通常取 1 个,并尝试窃取其他 P 的 G)。
  4. 执行 G:M 设置 curg 为获取到的 G,恢复其上下文并开始执行。
  5. G 阻塞或退出:
    • 若 G 主动让出(如 runtime.Gosched()),G 放回 LRQ 尾部,M 寻找下一个 G。
    • 若 G 发生系统调用阻塞,M 与 P 解绑,P 寻找新的 M 继续执行其他 G;阻塞的 M 等待系统调用返回后重新绑定 P。
    • 若 G 执行完毕,G 被回收至 P 的空闲 G 列表,M 继续取下一个 G。
性能陷阱: 如果 G 频繁发生系统调用(如大量文件 I/O、网络 I/O 未使用 epoll),会导致 M 频繁与 P 解绑和重绑定,增加调度开销。此时应使用异步 I/O 或调整 GOMAXPROCS 来缓解。

2.6 工作窃取(Work Stealing)机制

当某个 P 的 LRQ 为空时,该 P 绑定的 M 不会空闲等待,而是尝试从其他 P 的 LRQ 尾部窃取一半的 G(最多 128 个)。这种机制保证了负载均衡,避免某些 P 忙死而其他 P 空闲。

场景 窃取目标 窃取数量
本地 LRQ 为空 随机选择一个其他 P 目标 LRQ 长度的一半(上限 128)
所有 P 的 LRQ 都为空 全局运行队列 (GRQ) 1 个(若 GRQ 非空)
GRQ 也为空 M 进入自旋状态 等待新 G 被创建或唤醒

2.7 小结