之前在团队里做代码评审,看到一段用 goroutine 处理批量请求的服务代码,随口问了一句:你现在系统线程数大概有多少?同事愣了一下。他用 goroutine 写并发写得很溜,但真让他解释“为什么一个 Go 进程可以开几十万个 goroutine 还不把操作系统打挂”“这些任务底层到底是谁在调度”,他就开始含糊了。
这是很多 Go 开发者的共同状态:go func() 用了一年,可能都没完整读过一遍调度器相关的东西。Go 的并发核心从来不只是语法层面那个 go 关键字,而是运行时里的调度器。它用 GMP 三个角色把十几万任务安排得明明白白,靠工作窃取让所有 CPU 核都尽量不闲着。这篇我把 goroutine 从创建、运行、阻塞、被偷、再到继续跑的完整路径拆开讲,也会分享一些排查调度瓶颈时真正用得上的工具和实测思路。
1. 在拆 GMP 之前,先回答一个问题:为什么 Go 不直接用线程池
1.1 一个进程里跑十万个“任务”为什么还能活
要理解调度器,先理解 goroutine 和操作系统线程的本质差异。
系统线程是内核管理的执行体,创建线程要陷入内核态做一堆资源分配;线程栈默认就有几 MB 起步,Linux 上 ulimit -s 经常是 8MB。线程切换时,内核需要保存/恢复完整的寄存器上下文、更新调度数据结构、处理 TLB 和缓存失效,这些成本轻松到微秒级。线程一旦阻塞在系统调用上,整个线程就卡住,哪怕只是等一个网络读,也占着一个系统线程。
goroutine 完全不同。它是 Go 运行时自己管理的用户态协程,新 goroutine 初始栈只有 2KB 左右(具体值由 _StackMin = 2048 决定),随使用动态增长。创建成本极低,绝大部分情况就是内存里分配一个结构体,不需要进内核。goroutine 之间的切换发生在用户态,由运行时的调度器完成,开销比线程切换低一个数量级甚至更多。
有个很容易被误解的点:goroutine 虽然叫“协程”,但一旦遇到 channel 等待、mutex 竞争、网络 I/O 这类操作,它并不是“阻塞整个线程”,而是把自己挂起到等待队列,把当前的操作系统线程让出来,让调度器从队列里取另一个就绪的 goroutine 继续跑。这才是 Go 能支撑超高并发的根本原因。
我见过有人把 goroutine 类比成“轻量级线程”,这个说法很容易让人误以为它可以随便创建、没有代价。实际上它更像一个“待办任务单”,真正干活的人是操作系统线程。任务单可以有几万张,但同一时刻真正在 CPU 上执行的用户态 goroutine,最多不会超过 GOMAXPROCS 个。
1.2 用户态调度器要解决的三件事
既然不用线程池,Go 运行时就得自己解决几个问题:
- 谁来执行:几万个 goroutine 散落在内存里,必须有一个机制决定“下一个让哪个 goroutine 跑”。
- 阻塞怎么办:goroutine 在运行时可能等 channel、等锁、等网络包,也可能真的调用系统调用阻塞住。调度器要保证某些 goroutine 卡住时,其他 goroutine 还能继续跑。
- CPU 空闲时去哪找活:某个线程跑完了本地任务,不能就让 CPU 闲着,得想办法从别的地方“偷”任务过来。
GMP 模型就是围绕这三个问题设计的:G 表示 goroutine,M 表示操作系统线程,P 是调度器里的处理器资源。三者咬合在一起,构成了整个 Go 运行时的调度骨架。
提示:本文基于 Go 1.21 附近的主线源码来聊。不同小版本调度器的细节调整很多,比如 Go 1.22 之后本地队列和窃取策略有优化,但 GMP 这个宏观框架没有变,理解它对排查问题、读源码都够用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. G、M、P 到底分别是什么,各管哪一段
2.1 G:一张轻量的“任务状态卡”,不是一个执行体
G 在运行时里对应一个 g 结构体,它描述的是一个 goroutine 的执行状态。G 自己不会“跑”,它只是一张任务卡,上面记录了:
- 当前执行到哪条指令(
sched.pc、sched.sp等),也就是 goroutine 被暂停时的完整现场; - 栈信息(
stack),Go 的栈是动态增长的,结构体里维护了栈的边界; - 当前被哪个 M 执行(
m字段); - 当前处于什么状态(
atomicstatus); - 这个 goroutine 从哪里开始执行(
startpc)。
类比一下:G 就像办公室里的待办事项单,上面写着“这个任务做到哪一步了”。执行者 M 拿起一张任务单就开始干,干到一半如果碰到要等的事情,就把进度写回任务单,把单子放下,去拿另一张。
每个 goroutine 从创建到销毁,会经历一系列状态变化:_Grunnable(就绪,等着被调度)、`_Gru
