Go中GMP调度模型是如何优雅地应对G阻塞?

前言

「GMP 是如何优雅地应对 G 阻塞」,这是 Go 调度器最引以为傲的设计之一。我们接下来详细讲解 Go 调度器在 G阻塞时的完整流程逻辑,从不同场景触发、到资源切换、再到恢复调度,全流程解剖。


🧠 为什么要优雅处理 G 的阻塞?

如果一个 Goroutine(G)阻塞了,而它仍占据 M(线程)和 P(调度器)资源,那将:

  • 浪费系统线程

  • 阻碍更多 goroutine 被调度

  • 降低系统并发度和吞吐量

所以 Go 设计了一套 "快速摘下阻塞 G,调度其它 G" 的机制,以保持系统流畅运行。


🧰 一、G 阻塞场景的分类

Go 通过不同的策略分别处理以下几类阻塞场景:

阻塞类型 场景举例 M 是否阻塞? 特殊机制
用户态阻塞 channel、select、mutex ❌ 不阻塞 G 移除、M+P 继续调度
系统调用阻塞 net.Dial、文件I/O等 ✅ 会阻塞 M 进 syscall,启动新 M
时间阻塞 time.Sleep、定时器 ❌ 不阻塞 放入时间队列,按时唤醒
手动挂起 Gosched、yield ❌ 不阻塞 G 主动让出调度权

🔁 二、调度器的核心目标

  • 不让 G 的阻塞影响 M 和 P 的继续执行

  • 能快速调度其他 G 保证运行不中断

  • 阻塞 G 一旦就绪,能快速恢复调度


⚙️ 三、详细流程逻辑(展开讲解)

我们拆解为两个流程:

🧩 A. 阻塞发生时的处理流程

假设当前 M1 正在运行 G1,此时 G1 阻塞了:

🔹【步骤1】检测到阻塞点(用户态 or 系统调用)

例如:

  • ch <- val 阻塞

  • net.Dial()阻塞

  • mutex.Lock() 等待

🔹【步骤2】调度器标记状态
  • G1 被标记为 waiting/ syscall 等状态

  • 放入对应的等待队列(channel queue、等待互斥锁、netpoller 等)

🔹【步骤3】解绑 M 和 G
  • G1 从 M 和 P 脱钩

  • M 和 P 空出可以继续运行其它 G

🔹【步骤4】P 获取新 G
  • 从本地队列、全局队列或 work stealing 获取新的 G

  • 如果本地无 G,调度器考虑休眠 M,或尝试 global steal

🔹【步骤5】如果 syscall 阻塞 M
  • M 会进入 in-syscall 状态

  • 调度器创建或复用一个新的 M(如果当前活跃 M 数 < GOMAXPROCS)

  • 保证此 P 始终有 M 可用,不影响调度


🧩 B. 阻塞结束时的恢复流程

当之前阻塞的 G(如 G1)满足条件,恢复执行:

🔹【步骤1】G 状态改为 runnable

例如:

  • channel 有值

  • mutex 解锁

  • epoll/kqueue 返回可读/写事件

  • sleep 时间到

🔹【步骤2】唤醒 G
  • G1 被放入对应的 P 的本地队列(如果还绑定)

  • 如果没有活跃 M 与 P 绑定,会唤醒空闲的 M 来绑定并执行 G

🔹【步骤3】G 被调度执行
  • 通过正常调度流转,G1 会重新被某个 M 拿到继续执行

📜 四、系统调用阻塞的特殊处理逻辑(关键)

这是 GMP 调度中最巧妙的一环!

go 复制代码
func schedule() {
    if m.syscall {
        // 当前 M 正在 syscall(可能会阻塞)
        handoff P 给新 M
        mark m in-syscall
        start new M to keep P busy
    }
}

一旦 M 进入 syscall 且检测到潜在阻塞,P 会被剥离给新 M,保证系统调度继续,旧的 M 自己去系统调用,回来了再加入空闲 M 池。

🔄 五、阻塞/唤醒过程图示

go 复制代码
G1阻塞中(如读channel)
┌─────────────┐
│ M1执行G1    │
└────┬────────┘
     ↓
[G1 阻塞,进入等待队列] ──┐
                         ↓
           M1 脱钩,空出 P1
                         ↓
              P1 调度其他 G(如G2)
                         ↓
           G1 等待唤醒事件(如chan有值)
                         ↓
         [G1 变为runnable,重新入队]
                         ↓
           P1 再次调度 G1(或其他P抢占)

✅ 六、总结流程核心词汇(调度器关键词)

关键词 含义
waiting G 在等待资源(如channel)
syscall G 或 M 正在系统调用
runnable G 可以调度执行
in-syscall M 被系统调用阻塞中
handoff M 把 P 让给另一个 M
netpoller 异步网络I/O事件检测器

🎯 七、总结一句话

Go 的 GMP 调度器通过 挂起阻塞 G,解绑其占用的 M 和 P,调度其他 G 替代执行 ,在需要时再恢复原 G,从而优雅实现了非阻塞、高并发、极致资源利用的调度体系

相关推荐
研究司马懿19 小时前
【云原生】Gateway API高级功能
云原生·go·gateway·k8s·gateway api
梦想很大很大1 天前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰2 天前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘2 天前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤2 天前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt112 天前
AI DDD重构实践
go
Grassto4 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto5 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室6 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题6 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo