GMP模型是Go运行时内置的调度骨架,通过G(轻量协程)、M(OS线程)、P(逻辑处理器)解耦协同实现高并发;它不依赖外部库,每个go func()均由其调度,核心在于动态复用有限M支撑海量G,而非堆砌数量。

GMP 模型不是配置项,而是 Go 运行时内置的调度骨架。你在微服务里写的每一个 go func(),背后都由 GMP 三者协同调度,不依赖外部库或中间件——它就是 Go 本身。
为什么不能靠增加 Goroutine 数量来扛并发
很多人一见 QPS 上不去,第一反应是“多起几个 goroutine”。但实际中,go handler() 泛滥会导致:
- 本地队列(LRQ)溢出,G 被塞进全局队列(GRQ),触发锁竞争和缓存失效
- 大量 G 处于
_Grunnable或_Gwaiting状态,调度器频繁扫描却无实质工作可做 - GC 压力陡增:每个 G 至少持有栈帧和寄存器上下文,百万级 G 会显著拖慢 STW 阶段
- 网络 I/O 密集场景下,若未启用
netpoll(默认开启),仍可能因系统调用阻塞 M,但 P 已被偷走,造成隐性延迟
GOMAXPROCS 设置不当会直接浪费 CPU
GOMAXPROCS 决定 P 的数量,也即真正能并行执行用户代码的逻辑处理器上限。它的值直接影响:
- 设得太低(如
GOMAXPROCS=1):所有 G 强制串行,哪怕有 64 核也只用 1 个,P 的本地队列变成单点瓶颈 - 设得太高(如
GOMAXPROCS=1024):P 数远超 CPU 核心数,M 频繁在不同 P 间切换,Work Stealing 变成“Steal Overhead”,反而降低吞吐 - 默认行为是
runtime.NumCPU(),对微服务更稳妥;若混跑 CPU+IO 密集型任务,可略上调(如 +2~4),但绝不建议硬编码为固定大数
系统调用阻塞时,M 与 P 解绑的真实代价
当一个 G 执行 read()、write() 等阻塞系统调用时,M 会带着 G 进入内核等待,同时主动释放绑定的 P。此时:
立即学习“go语言免费学习笔记(深入)”;
- P 会被其他空闲 M “捡走”,继续消费本地队列里的 G —— 这是 Go 高并发的关键保障
- 但若该系统调用耗时极长(如慢磁盘 IO、未设 timeout 的 HTTP client),P 虽被复用,G 却长期滞留于
_Gsyscall状态,占用栈内存且无法被 GC 回收 - 更隐蔽的问题是:若大量 G 同时陷入阻塞,M 池可能快速扩容(最多到 10000),而 OS 线程创建本身就有开销,尤其在容器环境里易触发
pthread_create失败 - 正确做法是优先用
net/http默认的非阻塞模型(基于epoll/kqueue),对文件 IO 使用io.ReadFull+ context.WithTimeout,避免裸调syscall
真正难处理的从来不是“怎么起 goroutine”,而是“怎么让 goroutine 尽快结束”——GMP 调度器再高效,也救不了卡在锁、死循环或未关闭 channel 里的 G。


















