go并发依赖gpm模型实现用户态精细调度:g优先入当前p本地队列(≤256个),满则半数迁移至全局队列,再满则同步执行;g不绑定m或p,由空闲p/m动态获取;阻塞时m解绑p并休眠,p交由其他m接管,确保低开销;系统调用中m解绑p后由新m接管,netpoller统一处理非阻塞i/o;gomaxprocs默认为cpu核心数,计算密集型宜设为此值,i/o密集型可略高,但需避免设为1导致调度瓶颈。

Go 的并发不是靠操作系统线程堆出来的,而是靠 GPM 模型在用户态精细调度实现的——理解它,才能写出不卡死、不泄漏、不浪费 CPU 的并发代码。
goroutine 创建后到底去了哪?
你写 go f(),运行时不会立刻执行 f,而是先创建一个 G 结构体,然后按优先级塞进队列:
- 优先放进当前
P的本地队列(runq),最多存 256 个 - 本地队列满时,把其中一半挪到全局队列(
global runq) - 全局队列也满(极少见),就直接在当前
M上同步执行(避免阻塞调度)
注意:G 不绑定任何 M 或 P,它只是“待办任务”,谁空闲谁取。
为什么 goroutine 阻塞时 CPU 不飙高?
关键在 P 和 M 的解耦机制。当 G 因 channel、net.Read、time.Sleep 等进入阻塞:
- 运行它的
M会把G推入对应等待队列(如sudog链表),然后调用schedule() - 如果该
M没有其他可运行G,它就释放持有的P,自己休眠进findrunnable()循环 -
P不会被销毁,而是回到空闲列表,等待下一个唤醒的M来接管
也就是说:阻塞的 G 不占 M,也不锁 P,更不拉起新线程——这才是低开销并发的底层保障。
系统调用(syscall)怎么不拖垮整个 P?
传统线程模型里,一次 read() 阻塞就让整个线程挂住;GPM 把这事拆开了:
- 若
M进入阻塞式系统调用(如文件读、sleep),它会主动与P解绑 - 运行时检测到该
P还有可运行G,会立即唤醒或新建一个M来接管这个P - 非阻塞 I/O(如
epoll_wait)由netpoller统一处理,G直接挂起,M立刻回归调度循环
所以你在 HTTP server 里看到上万连接,背后可能只有几十个活跃 M ——但前提是别滥用 runtime.LockOSThread(),否则 M 被钉死,P 就真卡住了。
GOMAXPROCS 设置多少才合理?
默认值是 CPU 核心数,但这只是起点:
- 纯计算密集型服务(如图像转码),设为 CPU 核心数基本最优
- I/O 密集型(如 API 网关),可以略高于核心数(比如 ×1.5),给调度留点弹性
- 千万别设成 1——那所有
G都挤在一个P里,本地队列易满,频繁 fallback 到全局队列,锁竞争上升 - 动态调整可用
runtime.GOMAXPROCS(n),但注意:它只影响新创建的P,已存在的不会销毁
真正容易被忽略的是:GOMAXPROCS 控制的是 P 数量,不是线程数;M 数量由负载自动伸缩,上限受 runtime/debug.SetMaxThreads 约束——别只调 GOMAXPROCS 就以为搞定了并发资源。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











