Files
autumn-recruitment/00.Go/concurrency/Goroutine 调度模型_test.md

7.2 KiB
Raw Permalink Blame History

tags, create time
tags create time
test/review
go
goroutine
gmp-scheduler
work-stealing
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 个要点即可得满分;完全正确需覆盖全部要点。

关联笔记