Files
autumn-recruitment/00.Go/concurrency/Select 多路复用机制_test.md
T

227 lines
7.8 KiB
Markdown
Raw Normal View History

2026-08-09 19:06:40 +08:00
---
tags: [test/review, go, select, poll-multiplexing, fairness]
create time: 2026-08-09 12:00
---
# Select 多路复用机制_测试题
## 概述
本试卷覆盖 Go select 关键字的底层实现、随机公平性保证、nil channel 行为、default 分支语义、goroutine leak 场景及与 context 的组合用法,共 10 道题(6 选择 + 3 填空 + 1 简答)。
---
## 一、选择题(6道,由浅入深)
> **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景
### Q1(基础)— 考察定义层面
select 关键字在 Go 中能处理以下哪种操作?
A. 任意两个变量的比较运算
B. 多个 channel 的发送或接收操作
C. 多个 goroutine 的并行启动
D. 对 sync.Mutex 的加锁和解锁
### Q2(基础)— 考察行为判断
当 select 中有多个 case 同时满足条件时,Go 会如何处理?
A. 从上到下按顺序匹配第一个符合条件的 case
B. 从所有可匹配的 case 中随机选择一个
C. 优先选择 buffer 较大的 channel 对应的 case
D. 抛出一个 ambiguity error 让开发者修改代码
### Q3(进阶)— 考察原理理解
关于 nil channel 在 select 中的行为,以下描述正确的是?
A. 会导致 panic,程序崩溃
B. 该 case 永远不会被选中,类似于被永久禁用
C. 该 case 会被选中一次然后报错退出
D. 编译阶段就会报错,不允许写出这样的代码
### Q4(进阶)— 比较辨析
以下哪个选项**等价于**给 select 添加 default 分支的效果?
A. `case <-time.After(1 * time.Hour)` — 设置一个很长的超时
B. `case <-time.After(0)` — 设置超时时间为零
C. 将所有的 channel 改为有缓冲 channel
D. 使用 `for` 循环包裹 select 反复执行
### Q5(深入)— 场景推理
阅读下面代码,运行结果是什么?
```go
var ch chan int = nil
select {
case <-ch:
fmt.Println("received")
default:
fmt.Println("default")
}
```
A. 打印 "received"
B. 打印 "default"
C. 永久阻塞
D. 触发 panic
### Q6(深入)— 源码级/边界场景
以下哪种写法**不会**导致 goroutine leak?
```go
// A
func a() {
for {
select {
case v := <-ch:
process(v)
}
}
}
// B
func b(ctx context.Context, ch <-chan int) {
for {
select {
case v, ok := <-ch:
if !ok { return }
process(v)
case <-ctx.Done():
return
}
}
}
// C
func c() {
timer := time.NewTimer(5 * time.Minute)
// 没有调用 timer.Stop(),timer goroutine 持续运行
}
// D
func d() {
for {
select {
case <-time.After(1 * time.Second):
doSomething()
}
}
}
```
A. A
B. B
C. C
D. D
---
## 二、填空题(3道)
### F1 — select 内部结构
每个 select 语句在编译时被转换为一个 `selstruct` 结构体,其中 `[填空1]` 字段存储 case 关联的 channel 指针数组,`order` 字段存储随机化后的访问顺序。
> **提示**: 这个字段名是英文复数形式,对应 "channels" 的缩写。
### F2 — 随机性函数
Go 调度器中用于处理 select 多路复用的关键 netpoll 函数包括:`pollSurprise`(已注册 goroutine 突然可唤醒)、`pollRandom`(多选一时随机选择保证 `[填空1]`),以及 `pollDelay`(无 case 就绪时进入睡眠)。
> **提示**: pollRandom 的核心设计目的是防止某些 case 被系统性忽略。
### F3 — 性能对比
在有延迟的场景下,应当优先使用 `default` 分支而非 `time.After(0)`,因为 time.After() 背后会创建一个 `[填空1]`,即使是 0 延迟也有额外开销。
> **提示**: time.After 内部使用了定时器相关的系统资源。
---
## 三、简答题(1道)
### S1
某微服务中使用如下 Worker Pool 模式处理任务:
```go
func startWorkers(ctx context.Context, jobs <-chan Job) {
for i := 0; i < 10; i++ {
go func(id int) {
for job := range jobs {
process(job)
}
}(i)
}
}
```
该函数的调用者负责发送 job 到 jobs channel,但从未关闭过 channel。现在需要为这个 worker pool 增加优雅的停止能力。
请结合 select、context 和 channel 的知识,给出改进方案并说明至少三个关键点。
> **答题框架提示**:
> 1. 如何让每个 worker 感知到停止信号?
> 2. context 和 channels 如何配合使用?
> 3. stopper 通道的角色是什么?
---
## 参考答案与解析
### 选择题答案
| 题号 | 正确答案 | 解析 |
|------|---------|------|
| Q1 | B | select 只能用于 channel 相关操作(发送或接收)。A 应该用 if/else;C 用 `go` 关键字;D 用 Lock/Unlock。 |
| Q2 | B | select 的核心设计之一就是随机选择——当多个 case 同时就绪时,通过 pollRandom 随机选一个,避免优先级饥饿问题。如果没有随机性,总是固定从某个 case 开始匹配会导致系统性忽略其他 case。 |
| Q3 | B | nil channel 的 send/receive 本身会导致死锁(不是 panic),但在 select 中只是简单地不被选中,相当于永久禁用该 case。这不是编译错误,常被用于动态启用/禁用 case。 |
| Q4 | B | `time.After(0)` 会在 0 秒后立即触发定时器,等效于 default 的非阻塞语义。但 A 太长时间无效;C 不改变 select 的行为特性;D 反而会导致无限循环的空转。 |
| Q5 | B | nil channel 在 select 中永不匹配,所以 `<-ch` 不会被选中。由于存在 default 分支,立即执行 default,打印 "default"。这展示了 nil channel 作为 case 开关的用法。 |
| Q6 | B | A 无限循环且无退出路径,会 leak;C 未 Stop timer,Timer goroutine leak;D 每次迭代创建新的 Timer goroutine,永不停止则无限泄漏。只有 B 同时检查了 channel close(ok == false)和 ctx.Done(),有明确退出路径。 |
### 填空题答案
| 题号 | 答案 | 解析 |
|------|------|------|
| F1 | `chans` | selstruct 中包含 chans([]*hchan,case 关联的 channel 数组)、recv/send(收发地址数组)、order(随机化顺序)、ncases(case 数量)。 |
| F2 | `公平性` | pollRandom 的设计目的是保证多个可匹配 case 之间的公平性,防止某些 case 被系统性忽略(starvation)。面试中经常考"如果 select 按固定顺序匹配会发生什么"。 |
| F3 | `timer goroutine` | time.After() 内部创建了一个定时器 goroutine 来等待指定时长后发送值。即使超时时间为 0,这个 goroutine 也会被创建,产生不必要的开销。无延迟场景应直接 prefer default。 |
### 简答题参考答案
S1:**参考答案要点**:
1. **使用 context 传递取消信号**:worker 内部不应只用 `range`,而应改用 `for-select` 模式,同时在 case 中监听 `ctx.Done()`,这样 context 被取消时每个 worker 都能收到信号并返回退出。
2. **增加一个 stopper channel**:可以在 main/goroutine 中创建一个单独的 done channel,与 jobs channel 分开管理。当需要停止时,关闭 stopper channel,workers 检测到关闭后即退出。或者直接使用 ctx 的 Done channel。
3. **正确组合 select 与 ctx.Done()**:每个 worker 的 for 循环改写为:
```go
for {
select {
case job, ok := <-jobs:
if !ok {
return // jobs channel 被关闭
}
process(job)
case <-ctx.Done():
return // context 被取消
}
}
```
4. **调用方确保 cancel 被调用**:startWorkers 的调用者应在合适的时机(如 HTTP handler 返回时)调用 `cancel()`,并通过 defer 保证一定会执行。
**评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖全部要点。
## 关联笔记
- [[00.Go/concurrency/Select 多路复用机制]]