Files

161 lines
8.0 KiB
Markdown
Raw Permalink Normal View History

2026-08-09 19:06:40 +08:00
---
tags: [test/review, go, sync-primitives, futex, rwmutex-starvation]
create time: 2026-08-09 12:00
---
# Sync 包核心源码_测试题
## 概述
本试卷覆盖 Go sync 包的六大并发原语:Mutex(futex)、RWMutex(写饥饿模式)、WaitGroup(反 overflow 设计)、Once(双检锁)、Map(读写分离架构)和 Pool,共 10 道题(6 选择 + 3 填空 + 1 简答)。
---
## 一、选择题(6道,由浅入深)
> **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景
### Q1(基础)— 考察定义层面
sync.Mutex 的 state 字段中,bit[31](最高位)表示什么含义?
A. 当前等待者的数量
B. 是否有 waiter 在排队
C. locked 位(1 表示锁被持有)
D. 是否处于饥饿模式
### Q2(基础)— 考察行为判断
以下关于 sync.Once 的描述,哪一个是**正确**的?
A. Do() 中的函数会被所有调用者并行执行一次
B. 如果 Do() 传入的函数 panic,后续再次调用 Do() 会重新执行该函数
C. Do() 确保传入的函数只被执行一次,即使在高竞争下也是如此
D. Once 可安全地被复制使用,副本与原 Once 共享状态
### Q3(进阶)— 考察原理理解
sync.Mutex 的快路径是如何实现加锁的?
A. 通过 gopark 进入睡眠等待其他 goroutine 唤醒
B. 通过 atomic CAS 操作尝试将 state 设为 locked
C. 通过 spin-loop 自旋直到 state 变为 0
D. 通过读取系统调用获取内核态互斥锁
### Q4(进阶)— 比较辨析
关于 sync.RWMutex 的写饥饿(starving)模式,以下说法哪个是正确的?
A. 饥饿模式下锁直接移交给队列末尾的 writer
B. 触发饥饿模式的条件是等待时间超过 1ms 且已有等待者
C. 饥饿模式下读者和 writer 公平竞争,与普通模式无区别
D. RWMutex 的 ReadLock 是可重入的,与 Java 的 ReentrantReadWriteLock 一样
### Q5(深入)— 场景推理
阅读下面代码,运行时会发生什么?
```go
func main() {
var wg sync.WaitGroup
go func() {
wg.Done() // counter 初始为 0,Done 会使 counter 变 -1
}()
wg.Wait()
fmt.Println("done")
}
```
A. 正常打印 "done"
B. 触发 panic:negative counter in WaitGroup
C. 永久阻塞
D. 编译报错
### Q6(深入)— 源码级/边界场景
关于 sync.Map 的设计,以下哪个描述是**错误**的?
A. readOnly 用于读多场景,完全不需要持锁即可查询
B. amended=true 时,读取 miss 会先去 dirty map 查找并将 key 预热到 readOnly
C. readOnly 和 dirty map 同时包含相同的 key 时,优先返回 dirty map 的值
D. 当 missed 积累到一定程度时,dirty map 会被提升为新的 readOnly
---
## 二、填空题(3道)
### F1 — Mutex state 位布局
sync.Mutex 的 state 字段用三个区域编码:`state[31]` 是 `[填空1]` 位,`state[30]` 是 `[填空2]` 位,`state[0-29]` 存储等待者数量。
> **提示**: bit[31] 表示锁是否被持有,bit[30] 表示是否有等待者在排队。
### F2 — WaitGroup 反溢出设计
WaitGroup 内部使用 int64 分高低两段:高 32 位表示 `[填空1]`(goroutine 数量),低 32 位表示 `[填空2]`(阻塞在 Wait() 上的数量)。分开设计的目的是防止 counter 绕回 0 导致 Wait() 误判。
> **提示**: Add 减少高位计数,Wait 增加低位计数,两者互不干扰。
### F3 — Once 的双检锁
sync.Once 使用两层检查机制:第一层 `atomic.LoadUint32(&o.done)` 是无锁快速路径——绝大多数情况下 done 已经是 1,直接 return,零锁开销;第二层在 `doSlow()` 中加锁后再次检查 `if o.done == 0`,这是 `[填空1]` 防止多个同时通过第一层检查的 goroutine 重复执行同一个函数的保护机制。
> **提示**: 这被称为 Double-Check Locking 模式,两层检查缺一不可。
---
## 三、简答题(1道)
### S1
某电商服务需要实现一个商品库存计数器,要求:
- 多线程环境下安全递增/递减
- 读操作极其频繁(每秒数万 QPS),写操作相对较少
- 初始化逻辑只需执行一次
请说明你会分别选用 sync 包中的哪些原语来实现上述三种需求,并给出每种选择的理由和注意事项。
> **答题框架提示**:
> 1. 高频读 + 低频写场景用什么锁?为什么不选普通 Mutex?
> 2. 单次初始化用什么原语?它为什么比直接用 mutex 更高效?
> 3. 如果有批量操作或任务协调,如何结合 WaitGroup?
---
## 参考答案与解析
| 题号 | 答案 | 解析 |
|------|------|------|
| S1 | **参考答案要点**: | |
1. **sync.RWMutex**:读多写少场景应选 RWMutex 而非 Mutex。RWMutex 允许多个 reader 同时持有读锁,在高读低写时性能显著优于独占式的 Mutex。但如果读写比例接近或临界区极短,Mutex 反而更好(省去额外的位运算开销)。此外,RWMutex 的 ReadLock 不是可重入的,已持有读锁的 goroutine 再次请求会死锁。
2. **sync.Once**:单次初始化使用 Once 比手动配合 mutex 更简洁高效。Once 采用双检锁模式——第一次调用时才真正执行初始化函数,之后所有调用的 cost 几乎为零(只是一次原子 load)。注意:如果初始化函数 panic,Once 不会重试,done 仍为 1。
3. **sync.WaitGroup 的注意点**:如果涉及批量任务的等待和协调,可用 WaitGroup。但需注意:WaitGroup 不可复制(copy 后行为不可预测),应在启动 goroutine 前调用 Add(),且不能用作信号量(那是 buffered channel 的用途)。
**评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖全部要点。
### 选择题答案
| 题号 | 正确答案 | 解析 |
|------|---------|------|
| Q1 | C | A 对应 state[0-29](等待者数量),B 对应 state[30](waiter 位),D 是 RWMutex 的概念。sync.Mutex 只用三个位区域,最高位 bit[31] 是 locked 标志。 |
| Q2 | C | A 错:只会执行一次,不是并行执行多次;B 错:panic 后 done=1,后续直接跳过不再执行;D 错:Once 不可复制,Copy 后的行为不可预测。 |
| Q3 | B | Mutex 快路径通过 atomic CAS 尝试将 state 设为 locked,竞争激烈时几乎零开销。A 是慢路径(slowLock);C 不完全准确——Go 1.9+ 实现了自适应自旋(低竞争短暂自旋,高竞争立即休眠),但核心的加锁原语仍是 CAS;D 错误,Go Mutex 纯用户态实现。 |
| Q4 | B | A 错:饥饿模式下锁移交给队首 writer,不是末尾;C 错:饥饿模式下后续 reader 会被挡在外面,直接向队首 writer 移交;D 错:RWMutex 的 ReadLock 不可重入,这与 Java 不同。只有 B 描述了正确的触发条件。 |
| Q5 | B | WaitGroup 的 counter 初始为 0,Done() 会将 counter 减 1 变成负数。随后 Wait() 检测到 counter 不为 0 而阻塞,但由于没有其他 goroutine 再做 Add(positive),它会一直等到 panic(runtime 检测到 negative counter 会报 panic)。 |
| Q6 | C | sync.Map 的 Load 优先级:先查 readOnly → miss 且 amended=true 时查 dirty → 返回 not-found。**优先返回 readOnly 中的值**,而非 dirty。A/B/D 的描述均正确。这是 sync.Map 读写分离架构的核心设计之一。 |
### 填空题答案
| 题号 | 答案 | 解析 |
|------|------|------|
| F1 | `locked`,`waiter` | Mutex 的 state[31] = locked(1 表示持有),state[30] = waiter(1 表示有等待者),state[0-29] = 等待者数量。 |
| F2 | `counter`,`waiters` | WaitGroup 用 int64 分两段:高 32 位存 counter(Add 减少),低 32 位存 waiters(Wait 增加)。这样防止 counter 从 1 减到 0 时被另一个 Add(1) 立刻绕回 0 导致误判。 |
| F3 | `二次检查` | 双检锁的第二层检查在持有 mu.Lock() 后进行,确保只有一个 goroutine 能真正执行 f()。第一层无锁快速路径 + 第二层防重复 = 高性能且正确。 |
## 关联笔记
- [[00.Go/concurrency/Sync 包核心源码]]