05.05 Go 调度模型:从长连接等待到 G/M/P
返回第五卷 · OS 调度前置:03.04 · I/O 就绪前置:03.09 · 任务前置:05.01 · 内存模型前置:05.04
05.05 Go 调度模型:从长连接等待到 G/M/P
DeepTutor 初稿经技术与教学审阅后的静态课程。连接数、G/M/P 名称和时间线都是纸上教学模型;没有运行 Go、trace、网络服务、基准或站点。G/M/P 与 netpoll 的概念依据 Go 官方运行时文档和源码核对,具体队列、抢占和线程时机随 Go 版本与平台演进;本章不声称固定 OpenIM 版本采用这些纸上安排。
一、2000 条连接,不是 2000 个线程都在跑
设想虚构 IM 网关维护 2000 条长连接,其中大多数正在等客户端下一条消息。03.04 已区分“可运行”和“等待事件”,03.09 已解释网络 FD 可借就绪通知等待。连接仍会占用 FD、内核 socket 状态和应用缓冲;但仅凭 2000 这个连接数,不能推出同时有 2000 个 OS 线程正在用 CPU,也不能推断进程 RSS 必然低。
Go 还多一层调度:goroutine 是 Go 的并发执行任务,OS 线程才是操作系统调度到 CPU 的实体。Go 运行时把可运行的 goroutine 安排给可执行 Go 代码的线程;网络等待中的 goroutine 可暂时不占一个持续忙等的执行槽。不同 I/O 路径和外部调用有不同阻塞方式,后面再讲。
| 第一遍 | 第二遍 | 交付物 |
|---|---|---|
G/M/P 词汇、可运行/运行/等待、GOMAXPROCS | 本地/全局队列、系统调用、netpoll、公平与资源预算 | 一张 P=1 的 t0–t3 状态表和一份 IM 连接成本清单 |
先修是 05.01 的任务生命周期、03.04 的 OS 可运行/等待、03.09 的 FD 就绪;05.04 已强调调度先后不是共享变量的 happens-before。S4 的本章讲机制,05.10 才学习真实 trace 与性能证据。
二、G 的三种主状态:等待变就绪,也不是立刻完成
Go 运行时文档里的 G 是 goroutine。为入门先记三种课堂状态:
| 课堂状态 | 含义 | 虚构 IM 例子 |
|---|---|---|
| 可运行(runnable) | 有工作、具备运行条件,等调度到 Go 执行资源 | c-a 解析完输入,待计算权限 |
| 正在运行(running) | 此刻由某 OS 线程在持有 P 的条件下执行 Go 代码 | 正在校验一条 m-a 的字段 |
| 等待(waiting) | 需网络、channel、计时器或其他事件才能继续 | 等 u-b 的下一段网络字节 |
等待网络的 G 被通知后,一般先变成可运行候选,仍要等调度机会;它没有因为一个 FD 可读事件就自动完成短读、帧解析或 IM 业务确认。一个等待中的 G 也可能因取消、期限或连接关闭而被唤醒,唤醒原因仍要由代码判断。若处理者忘了响应取消或持有无限队列,“已经有调度器”不会替它结束任务。
这些是面向学习的状态类别,不是要求背运行时每一种内部状态枚举。G 的数量可以远大于同时执行 Go 代码的数量;一个 G 也不等同一条网络连接,具体是一连接一任务还是多任务协作由应用设计决定。
三、M 与 P:执行 Go 代码需要三者相遇
官方运行时的 M 代表 OS 线程,可能执行 Go 用户代码、运行时工作、系统调用或处于空闲。P 代表执行 Go 用户代码所需的调度与分配等资源/权利;G 要运行 Go 用户代码,需要与一个持有 P 的 M 结合。P 不是物理 CPU 核心,也不是 OS 线程本身。当前运行时文档把 P 的数量与 GOMAXPROCS 对应;这约束同时执行用户 Go 代码的并行度,而不是 goroutine 总数或线程总数。
先画最小纸上模型:设 GOMAXPROCS=1,只有 P1;M1 持 P1 正在跑 G-a(处理一小段 c-a 工作),G-b 在等网络,G-c 已可运行、在等调度。此刻只能有一个 M 持 P1 执行用户 Go 代码,但进程可以还有其他 OS 线程,尤其当某线程在系统调用中等待。
t0:M1 + P1 → 运行 G-a
G-c 在可运行队列
G-b 等网络事件这张图只标执行资格与状态,不是 Go 运行时实际线程编号。它还不能回答哪条业务消息先存储:G 的运行顺序不等于会话序号、数据库提交顺序或设备投递顺序。
四、运行队列与工作窃取:调度器不替业务排序
可运行的 G 需要排队。Go 运行时有与 P 关联的运行队列,也有全局工作来源;在多个 P 的场景,空闲 P 可尝试从其他 P 窃取可运行工作。目的在于让执行资源有活可做,而不是向应用承诺“先创建的 G 一定先执行”或“同一会话消息恰好按 goroutine 创建顺序提交”。本章不写死队列长度、窃取频率和精确抢占时刻,这些属于会变化的实现细节。
设 G-a 与 G-c 都已可运行。即使 G-a 先由 go 启动,G-c 仍可能在某次调度中先得到运行机会;G-a 运行中也可能遇到等待、让出或被抢占。调度顺序不可当作业务序号。 若 c-a 要给每条消息稳定 seq,就要在数据与事务层定义顺序并验证,不能靠“通常这个 goroutine 先跑”。05.04 的 happens-before 同样要靠显式同步边,而不是靠运行队列猜测。
工作窃取也不是无限公平保证。一个 G 长时间做 CPU 计算、频繁阻塞/唤醒或大量新任务涌入,都可能影响其他 G 的等待时长;需要以后用同版本的 trace 和业务时延数据观察,不能凭概念图宣布 P95 会下降多少。
五、阻塞系统调用:M 在等,P 可以去做别的工作
在 t0 模型上继续。假设 t1 时 G-a 进入一个会阻塞的系统调用,承载它的 M1 需要等待;运行时可把 P1 交给其他可工作的 M(纸上记作 M2),使可运行的 G-c 有机会继续执行 Go 代码。官方文档把“进入系统调用时 M 交还 P、恢复执行 Go 代码前需重新取得 P”列作 G/M/P 关系的关键机制。可移交不等于每个调用都必然立刻新建 M2,也不等于 G-c 在指定毫秒必定运行。
| 纸上时刻 | G-a | G-b | G-c | P1/M 的关系 |
|---|---|---|---|---|
| t0 | 在 M1+P1 上运行 | 等网络 | 可运行排队 | M1 持 P1 |
| t1 | 在阻塞系统调用中等待 | 等网络 | 获得运行机会的候选 | M1 等待;P1 可由 M2 持有 |
| t2 | 仍等或已返回,需重新获得 P | 网络事件到达,变可运行 | 运行或再次排队 | 谁先取得 P1 由调度决定 |
| t3 | 按实际唤醒条件继续 | 还需读字节和解帧 | 按实际工作继续 | 不从本表推断固定业务顺序 |
若 G-a 的系统调用返回时 P1 正在给 M2 使用,G-a 不能“无视 P”继续执行用户 Go 代码;它要重新进入调度过程。还有外部库/cgo、线程锁定及不同文件 I/O 路径,它们的成本和调度边界不能统统套用网络轮询故事。这里先说明为什么一个线程阻塞不必让所有可运行 Go 工作停下。
六、netpoll:网络可读使等待者可运行,不负责完整消息
Go 运行时包含网络 poller 的通用部分,平台上可对接 epoll、kqueue、Windows 等具体事件机制。对可被运行时轮询的网络描述符,当 G-b 等待 net.Conn.Read 所需的网络条件时,运行时可以登记等待并让 G-b 停放;内核报告就绪后,poller 把关联的 G 交回可运行流程。这个机制解释了为什么大量空闲连接不要求同数 OS 线程各自阻塞在一个传统 read 上。
G-b 请求读,但当前没有字节
→ 运行时登记 fd 的等待读就绪条件,G-b 等待
→ 设备/内核报告该 fd 可读,netpoll 让 G-b 具备运行条件
→ G-b 取得 P 后继续 Read,检查实际 n/错误
→ 应用累计字节、解析帧、再执行授权与业务步骤03.09 的重要边界在此继续成立:就绪不是完整帧,Read 返回不是设备业务确认。 G-b 被唤醒后也可能读到 EOF、错误或部分数据,且它先是可运行而非保证即时执行。不是所有阻塞调用都经同一 netpoll 路径;普通文件、cgo、系统调用或非轮询资源可能有不同实现条件。教材以 Go 运行时官方文档作概念参照,不从这里推断 OpenIM 某函数的具体调用图。
七、GOMAXPROCS、线程数和资源预算分别计什么
四个数要分开记:连接数计网络关系与 FD;goroutine 数计当前 Go 并发任务;OS 线程数计 M 等系统线程;GOMAXPROCS 约束同时执行用户 Go 代码的 P 数。虚构网关可有 2000 个连接、很多等待中的 G、少于连接数的 M,以及某个 GOMAXPROCS 值;不能把它们按等号相连。调整 P 数也不会自动修复慢接收方的无限输出队列、数据库等待、数据竞争或锁保护范围。
runtime.NumGoroutine() 是 goroutine 数的一次快照,不等于在线用户数或活跃消息数。CPU 不高而 RSS 上升,也可能来自连接对象、goroutine 栈、缓冲、TLS 状态或分配器保留;03.05–03.06 已说明 RSS 与 Go 对象需要分别观测。若怀疑调度等待,未来应把 Go trace 中的可运行/运行/阻塞时段与 03.10 的系统指标、11 卷的业务 P95 对齐;本章不实际运行 trace,也不宣称当前某机器有固定线程成本。
| 指标或对象 | 它控制/表示什么 | 不能替代什么 |
|---|---|---|
GOMAXPROCS / P 数 | 用户 Go 代码的并行执行资格 | goroutine 数、线程总数、连接上限 |
| M / OS 线程 | 实际线程及其运行、系统调用或等待 | 一个用户或一条 IM 消息 |
| G / goroutine | Go 并发任务与其状态 | 一定对应一个 FD 或一个设备 |
| FD/连接 | 内核与应用的连接资源 | CPU 正在执行任务的数量 |
八、交付 P=1 状态图与分层练习
学习者交付:画出 t0–t3 每个 G 的运行/等待/可运行状态与 P 的持有者;单独画 netpoll 把 G-b 从等待读转为可运行的路径;列出连接、G、M、P 四种数量不能画等号的反例。最后写明业务消息 seq、完整帧与设备确认分别在哪个更高层得到保证。已有 Go 基础的读者可依OpenIM 阅读地图定位固定版本的连接读写入口,逐条记录实际观察到的调用关系;本章没有检查该上游路径,不从纸图推出其线程数量。全部为纸上时间线,没有实际执行或性能数据。
分层练习:先答,再展开反馈
1. G 代表什么?
一个 goroutine,是 Go 调度的执行任务。
2. M 代表什么?
一个 OS 线程,可执行 Go/运行时工作,也可处于系统调用或空闲。
3. P 是物理 CPU 核心吗?
不是。它是执行用户 Go 代码需要的运行时资源/资格。
4. 当前运行时概念中 P 数与什么设置对应?
与 GOMAXPROCS 对应;不等于连接数或 goroutine 数。
5. G-b 等网络时在占用一个持续执行用户代码的 P 吗?
不占用。它等待条件,后续就绪再进入可运行流程。
6. 网络就绪后 G-b 一定立刻运行吗?
不一定。它先具备可运行条件,仍等调度与 P。
7. G-b 恢复 Read 就能宣布一条完整消息吗?
不能。还要检查 n/错误、累计字节并解析协议帧。
8. t0 的 P=1 模型里谁在运行 Go 用户代码?
M1 持 P1 运行 G-a;G-c 可运行排队,G-b 等网络。
9. t1 G-a 进入阻塞系统调用时,P1 可以怎样?
从等待的 M1 移交给其他可工作 M,让 G-c 有机会执行。
10. M1 系统调用返回后可不取得 P 就继续跑用户 Go 代码吗?
不可以;需重新取得相应执行资格。
11. GOMAXPROCS=1 是否意味着进程只有一个 OS 线程?
不意味着。阻塞调用、运行时与其他工作可涉及更多线程。
12. 2000 条连接就必有 2000 个 goroutine 吗?
不必然。连接与 goroutine 的对应是应用设计,不是语言定律。
13. 2000 个等待 G 就意味着 2000 个 M 正在忙等吗?
不意味着;可轮询的网络等待可停放 G,M 与 G 并非一对一。
14. P 本地队列和工作窃取给业务 FIFO 保证吗?
不给。它们是调度工作的方法,不是会话消息序号协议。
15. 先写 `go f()` 就保证 f 先于下一 goroutine 完成吗?
不保证。启动与完成先后要分开,05.04 用同步边解释。
16. 工作窃取主要试图解决什么问题?
让有空闲执行资源的 P 找到可运行工作,不让一处排队另一处长期空闲。
17. `runtime.NumGoroutine()` 能直接当在线用户数吗?
不能。它计 goroutine 快照,业务用户/设备语义不同。
18. GOMAXPROCS 增大就会自动消除慢接收方队列积压吗?
不会。慢设备流控、有限队列和满队列策略仍需单独设计。
19. CPU 不高可断定进程内存使用很低吗?
不能。连接、缓冲和对象可占驻留内存而任务大多等待。
20. 所有文件/外部库阻塞都与可轮询 net.Conn 完全一样吗?
不一样。资源与调用路径可能有不同阻塞和线程条件。
21. netpoll 就绪是否证明设备收到 IM 消息?
不能。它只提示 FD 有 I/O 条件,业务交付和确认另行定义。
22. 本章 P=1 的 M1/M2 时间线是已观测的 Go 排程吗?
不是。它是依据官方概念绘制的可发生纸上模型,实际时机需同版本 trace 验证。
来源与下一步
- Go 运行时 HACKING:Gs、Ms、Ps与
proc.go:调度角色、运行队列与系统调用的机制入口;运行时代码随版本演进。 - Go
netpoll.go与runtime文档:网络就绪、goroutine 与GOMAXPROCS的概念边界。 - Go
runtime/metrics与内存模型:运行状态观察入口及“调度先后不等于同步关系”。
按学习路线,下一章05.06 内存生命周期继续用消息缓冲解释栈/堆、逃逸、对象可达与内存布局。离开本章前,应能把连接 FD、等待/可运行 G、OS 线程 M、执行资格 P分别放到正确一层,并指出该图不能替代哪项 IM 业务顺序与确认保证。