148 lines
7.2 KiB
Markdown
148 lines
7.2 KiB
Markdown
|
|
---
|
|||
|
|
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 调度模型]]
|