--- tags: [test/review, go, goroutine, gmp-scheduler, work-stealing] create time: 2026-08-09 12:00 --- # Goroutine 调度模型_测试题 ## 概述 本试卷覆盖 Go GMP 调度模型的七大核心知识点:G/M/P 角色定义、状态转换、Local/Global RunQueue、Work Stealing、栈动态伸缩、GOMAXPROCS 影响及 goroutine 泄漏场景,共 10 道题(6 选择 + 3 填空 + 1 简答)。 --- ## 一、选择题(6道,由浅入深) > **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景 ### Q1(基础)— 考察定义层面 在 Go 的 GMP 调度模型中,P(Processor)的核心职责是什么? A. 真正执行 goroutine 代码的 OS 线程 B. 调度的本地资源,拥有 local runqueue 和执行权限 C. 用户级协程,包含栈和指令指针 D. 负责与操作系统内核交互的系统调用封装 ### Q2(基础)— 考察行为判断 一个 goroutine 进入 `Gwaiting` 状态的典型场景是? A. 刚被 runtime 创建完成,尚未放入任何队列 B. 正在等待 channel 读写或 mutex 锁 C. 绑定了某个 P,正在执行用户代码 D. 从 P 的 local queue 取出,准备开始运行 ### Q3(进阶)— 考察原理理解 关于 Work Stealing 机制,为什么空闲 P 从忙碌 P 的队列头部偷取 goroutine,而不是尾部? A. 头部的 goroutine 数量更多,一次能偷到更多 B. 头部元素需要先分配更大的栈空间 C. 头部是最近加入的 goroutine,时间局部性好,减少 cache 干扰 D. 尾部元素已经被其他 M 绑定了,不允许从尾部偷 ### Q4(进阶)— 比较辨析 Go 的 goroutine 与操作系统线程相比,以下哪个对比描述是正确的? A. goroutine 栈大小固定为 2KB,不可增长 B. goroutine 创建需要陷入内核态,成本与 OS 线程相当 C. goroutine 并发规模可达百万级,而 OS 线程通常在数千级别 D. goroutine 由内核调度器负责调度切换 ### Q5(深入)— 场景推理 某服务程序中观察到 goroutine 数量随时间持续增长且不会回落,最可能的原因是? A. GOMAXPROCS 设置过小导致 CPU 竞争激烈 B. 存在 goroutine leak:如 channel 无人接收、context 从未取消等 C. goroutine 栈自动扩容超过了 maxStackSize D. Work Stealing 机制导致 goroutine 在不同 P 之间频繁迁移 ### Q6(深入)— 源码级/边界场景 关于 Go goroutine 栈的动态伸缩,以下说法哪一个是**错误**的? A. minStackSize 在 64 位平台为 2KB B. maxStackSize 可达 ~1GB C. Goroutine 启动时就预先申请完整的 2KB 栈空间 D. 扩容时采用 old * 2 的策略,缩容采用 old / 2 --- ## 二、填空题(3道) ### F1 — GMP 数量约束 在一个 Go 程序中,P 的最大数量上限为 `[填空1]`;每个 P 维护的 local runqueue 长度为 `[填空2]`。 > **提示**: P 的数量由 GOMAXPROCS 控制,local queue 的长度是一个固定的常数。 ### F2 — 栈伸缩计算 假设一个 goroutine 当前栈大小为 8KB,在执行过程中连续发生两次 GrowStack(未超过 max),第一次扩容后栈大小为 `[填空1]` KB;若此时没有继续增长改为 ShrinkStack,则缩容后栈大小为 `[填空2]` KB。 > **提示**: 注意先扩容再缩容的顺序,不要搞反。 ### F3 — G 状态流转 一个 goroutine 的正常生命周期路径是:`Gidle` → `Grunnable` → `[填空1]` → `Grunning` → 如果发生系统调用会变为 `[填空2]`,系统调用完成后回到 `[填空3]`。 > **提示**: 按实际执行顺序填写缺少的三个状态名。 --- ## 三、简答题(1道) ### S1 考虑如下场景:一个 HTTP 服务端使用了大量 goroutine 处理请求,每个 handler 内部又派生了子 goroutine 做数据库查询和缓存读取。一段时间后服务变得极其缓慢,通过 pprof 发现 goroutine 数量从几百涨到了几万。 请分析可能导致 goroutine 暴增的三方面原因,并给出对应的修复策略。 > **答题框架提示**: > 1. 分别从 goroutine 泄漏的三类常见场景入手思考 > 2. 结合 Context 的生命周期管理分析 > 3. 结合 Channel 的收发动作分析 > 4. 最后提出监控建议 --- ## 参考答案与解析 ### 选择题答案 | 题号 | 正确答案 | 解析 | |------|---------|------| | Q1 | B | A 描述的是 M(Machine Thread),C 描述的是 G(Goroutine),D 不是 P 的职责。P 本质是调度的本地资源,持有 local runqueue。 | | Q2 | B | A 是 Gidle,C 是 Grunning,D 是 Grunnable 到 Grunning 的转换结果。Gwaiting 表示阻塞等待外部事件(channel IO、mutex、timer)。 | | Q3 | C | 工作窃取设计中,头部是最近加入的元素,具有更好的时间局部性。原 P 自己从尾部取旧元素,从头部偷可以最小化对原 P 的 cache 干扰。A/B/D 均不符合实际实现。 | | Q4 | C | A 错:goroutine 初始 2KB 但可动态伸缩到 1GB;B 错:goroutine 在内核外调度,创建成本极低;D 错:由 Go runtime 调度而非内核。只有 C 正确描述了并发规模的差异。 | | Q5 | B | goroutine 持续增多是典型的 goroutine leak 信号——如 channel 永远无法完成收发、context 不取消导致 goroutine 无法退出等。A 导致性能低但不引起数量增长;C 不可能(不会超 max);D 只是正常现象。 | | Q6 | C | Go 1.4+ 采用段式内存分配,goroutine 首次需要时才申请小段按需增长,不是一开始就预占 2KB。A/B/D 的描述均符合文档。这是常见的认知误区。 | ### 填空题答案 | 题号 | 答案 | 解析 | |------|------|------| | F1 | `1024`,`256` | P 最大数量为 1024(GOMAXPROCS 限制),每个 P 的 local runqueue 固定长度为 256。 | | F2 | `16`,`8` | GrowStack:8KB * 2 = 16KB;ShrinkStack:16KB / 2 = 8KB。按顺序先扩后缩,回到原值。 | | F3 | `Grunnable`,`Gsyscall`,`Grunnable` | 标准流程:Gidle(空闲) → 放入 runqueue → Grunnable(就绪) → 被 M 取出 → Grunning(运行) → 发生系统调用 → Gsyscall → 返回后重新变为 Grunnable。 | ### 简答题参考答案 S1:**参考答案要点**: 1. **Context 未传播取消信号**:handler 内的子 goroutine 监听了 ctx.Done(),但如果父 context 设置了 timeout 但未在所有层级传递 cancel(),或者某些 goroutine 没有检查 ctx.Done(),它们就会永远运行下去。修复:确保所有层级的 goroutine 都监听 ctx.Done(),并在 handler 结束时 defer cancel()。 2. **Channel 死锁/无人接收**:向无缓冲或被无限写入的 channel 发送数据,或 goroutine 等待一个永远不会收到数据的 channel recv,都会使 goroutine 永久阻塞。修复:使用 select + ctx.Done() 模式,或在合适的时机关闭 channel。 3. **定时器未清理**:使用 time.After() 或 time.NewTimer() 时未调用 Stop(),导致定时器回调 goroutine 泄漏。修复:使用 defer timer.Stop(),或在不再需要时主动停止。 4. **pprof 监控**:使用 `go tool pprof -inuse_goroutines` 定期检查活跃 goroutine 数量趋势,异常增长应立即告警。 **评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖全部要点。 ## 关联笔记 - [[00.Go/concurrency/Goroutine 调度模型]]