Go GMP 调度器解析
写在前面 写过几年 Go 后再问"goroutine 是怎么跑起来的",很多人能说出"GMP 模型"四个字,但再往下追问就开始发虚: P 状态机有几个状态?1.26 为什么删掉了一个? go func() {} 编译后到底做了什么?G 是从哪里分配的? 一个 goroutine 卡死在死循环里,runtime 怎么把它"抢下来"?sysmon 能停一个无函数调用的循环吗? 网络 I/O 为什么不会阻塞 M?netpoller 怎么和调度器集成的? 这篇聊 GMP 的核心流程。源码按 Go 1.26 来看,主要会用到 runtime/proc.go 和 runtime/runtime2.go。 一、为什么需要用户态调度器 操作系统已经有线程调度了,为什么 Go 还要再造一个? 1.1 1:1 模型的代价 Java、C++、Rust 默认用 1:1 模型——一个用户线程对应一个内核线程。这意味着: 栈大:Linux 默认线程栈 8MB,10k 线程就吃掉 80GB 虚拟内存(虽然有 lazy commit,但限制 fd/线程数仍是问题)。 切换贵:内核线程切换要进入内核调度,保存和恢复线程上下文,通常是微秒量级。 创建慢:clone() 系统调用也是微秒量级,无法支撑"每个请求一个 goroutine"的编程范式。 Go 想做的事是:让 go func() {} 成为足够便宜的并发原语。这要求创建一个并发单元的开销在 微秒级以下,栈空间在 KB 级别——OS 线程做不到。 1.2 N:1 协程的局限 另一个极端是 N:1:一堆协程跑在单线程上(早期 Python gevent、Node.js 的 event loop)。问题是: ...