一个线程放锁后立刻又拿到,另一个线程等了几千次
主线位置:主线 1 的解锁分支。不公平的
unlock_slow把状态写成10并叫醒一个停车者,被叫醒的线程要回到lock_slow开头去抢;解锁方此刻还在 CPU 上,下一条指令就是lock(),几乎总能抢在前面(RawMutex 的一字节状态机)。
sequenceDiagram participant A as 线程 A(循环持锁) participant M as 状态 participant B as 线程 B(停车中) A->>M: unlock_slow:写 10,叫醒 B(TOKEN_NORMAL) A->>M: 立刻 lock():CAS 拿到锁 B->>B: 醒来(约 1–2 µs 后) B->>M: 抢锁失败,再停车 Note over A,B: 0.12.5:桶上的计时器 0–1 ms 内到期,下一次解锁强制交接 Note over A,B: 0.13.0 / 本机 std:没有这一步
现象
两个(或少数几个)线程用同一把锁,其中一个在循环里「加锁 → 干一段活 → 解锁 → 立刻再加锁」。另一个线程的进度远低于预期,甚至完全停滞。升级到 parking_lot 0.13 后,原来还算均匀的程序突然出现这种情况。
复现
E3(exp-parking_lot/eventual-fairness 与 exp-parking_lot/v013-eventual-fairness,同一份源码,不开 deadlock_detection):2 个线程循环加锁、记录持有者、持锁忙等 hold、解锁,锁外不做事。统计同一个线程最长连续拿到锁的次数(3 轮):
| 持锁 | 总次数 | 0.12.5 Mutex | std(本机 pthread) | 0.13.0 Mutex |
|---|---|---|---|---|
| 10 µs | 100 000 | 95 / 95 / 366 | 8 388 / 21 950 / 37 108 | 17 048 / 17 753 / 19 805 |
| 100 µs | 100 000 | 42 / 17 / 10 | 13 974 / 11 217 / 10 362 | 53 351 / 99 563 / 83 217 |
| 1 ms | 2 000 | 1 / 1 / 1 | 2 000 / 1 545 / 1 778 | 2 000 / 2 000 / 2 000 |
| 10 ms | 300 | 1 / 1 / 1 | 299 / 300 / 268 | 300 / 300 / 300 |
0.12.5 把连续独占限制在约 1 ms(10 µs × 95),持锁 ≥ 1 ms 时严格交替;0.13.0 和本机 std 都出现过一个线程拿走全部次数。反向验证:unlock_fair / FairMutex 在 4 线程、持锁 1 µs 时自取比例恒为 0(完整表见 公平解锁、bump 与 FairMutex)。
源码出处
- 插队:
lock_slow第一步「Grab the lock if it isn't locked, even if there is a queue on it」(raw_mutex.rs:209-289)。 - 兜底:
unlock_slow的回调里force_fair || result.be_fair为真才交接(291-323 行);be_fair来自桶上的FairTimeout,到期后把下一次定在 0–1 ms 之后(core/src/parking_lot.rs:98-149)。计时器是每个桶一个,只在真的叫醒了线程时才看(eventual fairness)。 - 0.13.0 删掉了这套兜底:CHANGELOG「Removed automatic eventual fairness. Fair unlocking remains available through the explicit fair-unlock APIs.」(#533 中的
8cfdedb,去掉的与修正的)。
怎么改
公平要显式要,三个 API 在 0.12.5 和 0.13 里都有:
- 长循环里定期让一下:
MutexGuard::bump(&mut g)。有人在停车时公平地交出去再排队拿回;没人等时只是一次Relaxed读。 - 这一次解锁要公平:
MutexGuard::unlock_fair(g),有人停车就直接交接,锁不经过「放开」状态。 - 整把锁都要公平:换成
FairMutex。代价是吞吐:E3 中 4 线程、持锁 1 µs 时从约 60 万次 / 秒降到约 36 万次 / 秒。注意FairMutex的守卫不能配Condvar(映射守卫与 FairMutex 配 Condvar)。 - 结构上避免:持锁时间长又频繁重入的循环,往往可以改成「锁里取一批、锁外处理、锁里放回」,持锁时间短了,插队的影响也小。
小结
- parking_lot 的
Mutex允许插队,放锁后立刻再加锁的线程常常自己抢回去。 - 0.12.5 有最终公平兜底,连续独占约 1 ms 封顶;0.13.0 去掉了,本机 std 也没有,E3 中一个线程可以拿走全部次数。
- 要公平就显式用
bump、unlock_fair或FairMutex,FairMutex约让吞吐降到 6 成。 - 依赖过 0.12 兜底的代码,升级 0.13 前要先改。
💬 关联讨论
发起讨论加载中...