Go中GMP调度模型详解(通俗易懂)

总结图(在充分理解GMP调度模型相关知识后,再回过头来看)

前言

Golang 的 GMP 调度模型 是 Go 语言运行时(runtime)中的核心机制之一,它高效地实现了 高并发协程(goroutine)调度执行,解决了 用户态调度(goroutine)与内核态线程(OS thread)之间的映射问题。下面我们从多个角度进行详细剖析:

🧠 一、什么是 GMP 模型?

GMP 模型是 Go 语言调度器的三个核心组件的缩写:

缩写 全称 作用
G Goroutine 用户态的轻量级线程,待调度的任务
M Machine 内核线程,用于执行 G
P Processor 执行 G 的上下文(调度器中的资源)

GMP 模型的作用是:
  • 将海量的 G(goroutine)以低成本、高效率的方式调度到 M(内核线程)上执行,通过 P(调度器上下文)协调,实现 M 和 G 之间的解耦。

我再换个更加通俗易懂的解释:
缩写 本质含义 类比身份 核心作用 功能
G Goroutine 乘客 需要被执行的任务/协程 想去一个地方的用户(要被执行的协程)
M Machine(OS Thread) 出租车司机 实际去执行 G 的执行者 能接送乘客的资源(OS线程)
P Processor 派单平台(车队) 任务分配器+运行上下文(调度器) 知道所有乘客在哪,决定哪个乘客配给哪辆车(调度器)

GMP与CPU核心数的关系:
  • 在 Go 中,P 的默认数量是 等于 CPU 核心数 的

  • 它的值由 GOMAXPROCS 控制,例如 4 核 CPU,默认 P=4

  • P 控制并发度,也就是说:最多只能同时有 P 个 G 被调度到M执行

对象 与 CPU 核心数一致? 控制并发度?
P ✅(GOMAXPROCS)
M ❌(动态创建回收)
G ❌(可以有很多)

🏗️ 二、组成部分详解

1. G(Goroutine)

  • 描述一个任务或函数(Goroutine)

  • 拥有自己的栈、指令指针、调度状态

  • 不直接绑定 OS 线程,轻量、创建开销小

2. M(Machine)

  • 对应一个 OS 线程

  • 负责执行 G 中的代码

  • M 不直接管理 G,执行时需要绑定一个 P

3. P(Processor)

  • 核心调度器资源,表示 CPU 上下文(非物理 CPU)

  • 每个 P 有自己的 G 队列(本地运行队列)

  • Go 程序启动时,P 的数量由 GOMAXPROCS 决定


🔁 三、GMP 调度过程详解

1、启动阶段

  • 启动时初始化多个 P,每个 P 尝试找 M 来绑定运行

  • M 必须绑定一个 P 才能执行 G,单独的 M 无法调度 G

2、G 的创建

  • 当用户调用 go func() 时,创建 G(Goroutine)

  • G 被加入当前 P 的本地队列或全局队列(满时)

3、M 调用 P 执行 G

  • M 绑定一个 P,从 P 的队列中取出 G 执行

  • G 执行完成或发生阻塞(例如 syscall),M 会进行下一轮调度

4、G 被挂起或阻塞(GMP调度模型是如何优雅地应对G阻塞)

  • 若 G 调用 syscall 等阻塞操作:

    • M 可能阻塞,P 会被解绑

    • 调度器会创建新 M 来继续执行剩余的 G

5、G 的抢占

  • 为了防止 G 长时间执行,Go 1.14 起引入 异步抢占,防止"长任务"阻塞调度

  • 运行时间长的 G 会被强制抢占,挂起后重新入队

6、调度策略:Work Stealing

  • 如果一个 P 的 G 队列空了,会尝试从其他 P 偷一半 G(work stealing)

  • 避免某些 P 长期空闲,提升 CPU 利用率


🧪 调度分析示意图(简化) --- 直接看最上面的总结图也可以

go 复制代码
+-------------+       +------------+      +-------------+
|   G1,G2...  | <---> |     P1     | <--> |     M1      |
+-------------+       |  本地队列  |      +-------------+
                      +------------+      ↑ 执行G逻辑    ↓
                                          +-------------+
                                          |     M2      | (阻塞中)
                                          +-------------+

    全局队列  ↔   所有 P 共享(G 多时平衡)

🧩 四、关键调度策略与优化点

策略/优化名 含义与作用
Work Stealing 空闲的 P 会从其他 P 偷 G(偷一半)
G 抢占 避免长时间占用 CPU,异步抢占机制
syscall 处理 阻塞 syscall 会释放 P,由新的 M 接管
GMP 池机制 尽可能复用 M(线程)和 G,降低创建成本
全局 G 队列 本地队列满时,G 会进入全局队列

⚠️ 五、特殊情况处理分析(重要)

1. ✅ M 找不到可用的 G

  • 尝试从本地队列取 G

  • 如果本地队列为空,尝试:

    • 从 全局 G 队列 取任务

    • 从其他 P 偷任务(Work Stealing)

  • 若还是找不到,就将 M 停止(进入空闲)

2. ✅ G 进行系统调用导致 M 阻塞

  • G 阻塞后,M 被挂起,P 被解绑

  • runtime 会创建新的 M,将 P 绑定过去继续调度其他 G

  • 原阻塞 M 等待 syscall 完成后再回归

3. ✅ 没有足够的 M 可用(如高并发)

  • Go runtime 有一个 M 的最大上限(默认 10,000)

  • 超过后会排队等候,防止线程资源耗尽

4. ✅ GC(垃圾回收)调度干扰

  • GC 会标记 G 状态,暂停一部分 Goroutine

  • Go 实现了 STW(stop the world)阶段的优化,减少调度阻塞

5. ✅ goroutine 爆炸(G 数量极多)

  • 可能导致本地队列、全局队列频繁切换

  • Go 内部通过"调度阈值"等机制控制调度频率


🔍 六、调度状态枚举(G 的状态)

状态 含义
_Gidle 空闲状态,未使用
_Grunnable 就绪状态,等待调度
_Grunning 正在运行
_Gwaiting 等待中(如channel)
_Gsyscall 正在执行系统调用
_Gdead 已终止

✅ 七、GMP总结

内容
模型优势 高并发、高性能、用户态调度、轻量线程
核心机制 GMP 三者协调:G 被放入队列,M 通过 P 执行 G
关键能力 Work Stealing、Syscall解绑、异步抢占
常见问题处理 M 阻塞、G 队列空、GC 干扰、G 暴增

📌 八、补充:如果 M在P的队列内找不到G来执行,会发生什么(详解)

🧩 一、场景还原

  • 设想以下情况:

    • M1 已经绑定了 P1

    • P1 的 本地 G 队列是空的

    • M1 正在尝试从队列中拿 Goroutine 来执行

那么,此时 Go 调度器会按照下面的步骤尝试"找点儿活干":

🔁 二、调度器的应对步骤(按优先顺序)

✅ 1. 检查全局 G 队列
  • 如果 P1 本地队列为空,调度器会先查看 全局 G 队列(global run queue) 是否有 G 可执行

  • 如果有,从全局队列获取一些 G(默认最多抢 61 个),加入本地队列,然后执行其中一个

✅ 2. Work Stealing(从其他 P 偷 G)
  • 如果全局队列也空,调度器会随机选择一个其他的 P,尝试从它的本地队列中"偷" 一半的 G

  • 如果偷到了,把偷到的一半 G 放到自己的 P1 的队列中,然后执行其中一个

✅ 3. 如果真的找不到任何 G:

此时,进入 "找不到活干"的处理逻辑 👇

🚫 三、没有 G 可执行时的最终处理流程

🧘‍♂️ M 进入休眠(park)
  • 如果找不到任何 G(本地、全局、其他 P 全都没有),则当前 M 会调用 runtime.park()

  • M 就此进入休眠状态,不占用 CPU

🗄 P 会保持空闲(进入 idle 状态)
  • 当前绑定的 P 会继续空转等待新的 G 进入队列(例如其他线程创建 goroutine)

  • 此时它仍然是活动状态,只是处于 idle

🔔 后续触发机制:唤醒 M
  • 一旦新的 G 被创建(通过 go 关键字),会被加入某个 P 的队列中

  • 如果该 P 没有绑定正在运行的 M,则会从 空闲 M 池中唤醒一个 M 来绑定这个 P

  • 重新进入调度循环

📌 四、完整流程图解

go 复制代码
┌────────────┐
│ M 绑定了 P │
└────┬───────┘
     ↓
┌────────────┐
│ P 本地队列空│
└────┬───────┘
     ↓
┌────────────┐
│ 查全局队列 │───┐
└────┬───────┘   │(有G→执行)
     ↓           │
┌────────────┐   │
│ Work Steal │───┘
└────┬───────┘
     ↓
┌────────────┐
│ 无 G 可执行 │
└────┬───────┘
     ↓
┌────────────┐
│ M 进入休眠 │
└────┬───────┘
     ↓
┌───────────────┐
│ 等待有新 G 创建 │←────┐
└───────────────┘     │
                      │(go func())
                      ↓
               ┌────────────┐
               │ 唤醒空闲 M │
               └────────────┘

🧠 五、这一设计的意义

  • 高效节能:当无任务时 M 自动休眠,不浪费 CPU

  • 资源复用:M 和 G 都可复用,避免频繁创建销毁系统线程

  • 响应及时:有新任务时能快速唤醒 M,响应调度

  • 负载均衡:work stealing 实现多核协同

相关推荐
GetcharZp26 分钟前
「Golang黑科技」RobotGo自动化神器,鼠标键盘控制、屏幕截图、全局监听全解析!
后端·go
围开哥哥42 分钟前
AI学习笔记 — RAG 与 中医知识的碰撞
go
程序员爱钓鱼3 小时前
Go同步原语与数据竞争:原子操作(atomic)
后端·面试·go
没逻辑18 小时前
Go 内存逃逸与泄漏排查实战
go
卜锦元19 小时前
Go中GMP调度模型是如何优雅地应对G阻塞?
go
qqxhb19 小时前
零基础设计模式——行为型模式 - 观察者模式
java·观察者模式·设计模式·go
asyncrustacean20 小时前
有栈协程基本原理和实现
后端·rust·go
WHOAMI__1 天前
Go轻松构建WebSocket服务器:EasyWS让一切变简单
go
aiee1 天前
Go 语言:高并发编程的性能突围之路
后端·go