8.0 KiB
tags, create time
| tags | 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(深入)— 场景推理
阅读下面代码,运行时会发生什么?
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 包中的哪些原语来实现上述三种需求,并给出每种选择的理由和注意事项。
答题框架提示:
- 高频读 + 低频写场景用什么锁?为什么不选普通 Mutex?
- 单次初始化用什么原语?它为什么比直接用 mutex 更高效?
- 如果有批量操作或任务协调,如何结合 WaitGroup?
参考答案与解析
| 题号 | 答案 | 解析 |
|---|---|---|
| S1 | 参考答案要点: |
-
sync.RWMutex:读多写少场景应选 RWMutex 而非 Mutex。RWMutex 允许多个 reader 同时持有读锁,在高读低写时性能显著优于独占式的 Mutex。但如果读写比例接近或临界区极短,Mutex 反而更好(省去额外的位运算开销)。此外,RWMutex 的 ReadLock 不是可重入的,已持有读锁的 goroutine 再次请求会死锁。
-
sync.Once:单次初始化使用 Once 比手动配合 mutex 更简洁高效。Once 采用双检锁模式——第一次调用时才真正执行初始化函数,之后所有调用的 cost 几乎为零(只是一次原子 load)。注意:如果初始化函数 panic,Once 不会重试,done 仍为 1。
-
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()。第一层无锁快速路径 + 第二层防重复 = 高性能且正确。 |