去掉的与修正的
主线位置:不在主线上。这一页是升级清单:0.13.0 删掉了 0.12.5 的哪些行为,删掉后用什么代替;#533 修了哪些问题,在 0.12.5 的哪几行。读法和 crossbeam 卷的 版本差异 相同:以 CHANGELOG 为纲,逐条对代码。
flowchart TB
subgraph removed["去掉的"]
ef["自动的最终公平<br/>8cfdedb"] --> efr["改用 unlock_fair / FairMutex / bump"]
hle["hardware-lock-elision<br/>16ebdb7"] --> hler["无替代(本机 aarch64 上本来就不生效)"]
rr["RwLock::read_recursive 与两个 trait<br/>8150554"] --> rrr["RecursiveRwLock:整把锁读者优先"]
end
subgraph fixed["#533 的修复"]
f1["unlock_upgradable_fair 不交接<br/>335d596"]
f2["wait_while 超时不复查谓词<br/>5fd0ddb"]
f3["generic parker 忙等<br/>4a3656f"]
f4["Apple 超时用墙上时钟<br/>c175220"]
f5["其余:只列原文"]
end
去掉自动的最终公平
CHANGELOG 原文:「Removed automatic eventual fairness. Fair unlocking remains available through the explicit fair-unlock APIs. This removes UnparkResult::be_fair. (#533)」,对应提交 8cfdedb「Remove eventual fairness」,提交说明为空。
0.12.5 里这套机制的位置(机制本身见 eventual fairness):
| 部分 | 0.12.5 位置 |
|---|---|
每个桶一个 FairTimeout,should_timeout 到期后把下一次定在 0–1 ms 之后 | core/src/parking_lot.rs 第 98–149 行 |
UnparkResult::be_fair 字段 | 第 510 行 |
三处计算:unpark_one、unpark_requeue、unpark_filter | 第 766 行、第 974 行、第 1072 行 |
消费方:be_fair 为真时改走 TOKEN_HANDOFF | src/raw_mutex.rs 第 299 行、src/raw_rwlock.rs 第 654 行、第 804 行 |
0.12.5 中,一个线程放锁后立即重新加锁,如果有人在排队,它最多连续独占到这个桶的计时器到期(0–1 ms 之内的某个时刻),之后的那次解锁会被强制交接。0.13.0 没有这个上限。
E3 的结果(方法和完整表格见 公平解锁、bump 与 FairMutex:exp-parking_lot/eventual-fairness 与 exp-parking_lot/v013-eventual-fairness,同一份源码,都不开 deadlock_detection;T 个线程循环「加锁、记录持有者、持锁 hold、解锁」,锁外不做事、立即重新加锁;统计同一线程最长连续拿到锁的次数;M2 Max,每组 3 轮)。T=2 时:
| 持锁 | 总次数 | 0.12.5 最长连续(3 轮) | std(macOS)最长连续 | 0.13.0 最长连续 |
|---|---|---|---|---|
| 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:持锁 10 µs 时最长 95 次,约 1 ms,正是
FairTimeout的上限(一轮出现 366,约 3.7 ms,原因的推测见上面链接的那一页);100 µs 时最长 10–42 次(1–4 ms)。持锁 1 ms、10 ms 时最长连续恒为 1,两个线程 50 / 50 严格交替:每次解锁时离上一次公平解锁都已超过 1 ms,每次都交接。 - 0.13.0:没有任何上限。持锁 1 ms、10 ms 时三轮都是一个线程拿走全部次数(份额 100 / 0);100 µs 时有一轮 99 563 / 100 000 次被同一个线程连续拿走。
- 本机 std(pthread)多数时候也接近独占,和 0.13.0 是一类行为。
T=4、持锁 1 µs 时两版看不出区别(0.12.5 最长连续 89–148,0.13.0 56–98):等待者多、持锁短时,计时器很少有机会起作用。差别出现在「少数线程、长临界区、放锁后立即重新加锁」的模式里:在 0.12.5 上这种代码实际得到的是近乎轮转的公平,升级到 0.13.0 后变成一个线程把另一个饿上整整一轮实验。
升级时怎么办:依赖过 0.12 这层兜底的代码(例如一个线程在循环里持锁处理一大批数据、每批之间放一下锁),要显式选择公平:
- 在那个循环里用
MutexGuard::bump(&mut g)代替「放锁再加锁」:有人排队时公平地让一次。 - 或者在放锁时用
MutexGuard::unlock_fair(g)。 - 或者整把锁换成
FairMutex,每次解锁都交接。E3 中 4 线程、持锁 1 µs 时,unlock_fair/FairMutex把吞吐从约 60 万次 / 秒降到约 36 万次 / 秒,自取比例为 0%,代价见 公平解锁、bump 与 FairMutex。
这三个 API 在 0.13 中都保留(CHANGELOG 原话「Fair unlocking remains available」)。
去掉 hardware-lock-elision
CHANGELOG:「Removed the hardware-lock-elision feature. (#533)」;提交 16ebdb7「Remove hardware lock elision support」,提交说明为空,上游没有写理由。
0.12.5 里它只在 x86 / x86_64、开了 feature、且不在 Miri 下时生效(src/elision.rs 第 25–33 行):
pub fn have_elision() -> bool {
cfg!(all(
feature = "hardware-lock-elision",
not(miri),
any(target_arch = "x86", target_arch = "x86_64"),
))
}
用到它的只有 RawRwLock 的读锁快路径(例如 src/raw_rwlock.rs 第 525–528 行,空锁时用带 xacquire 前缀的 CAS)和 unlock_shared。本机 aarch64 上 have_elision() 恒为假,这个 feature 开不开生成的代码都一样(状态字与两条队列)。0.12.5 的最后一个相关条目是 CHANGELOG 里的「Fixed Miri when the hardware-lock-elision feature is enabled (#491)」。
升级影响:依赖里还写着 features = ["hardware-lock-elision"] 的 crate 在 0.13 下会因为 feature 不存在而无法解析依赖;没有替代 API。
read_recursive 变成 RecursiveRwLock
CHANGELOG:「Replaced the recursive read methods on RwLock with a dedicated reader-biased RecursiveRwLock type. The RawRwLockRecursive and RawRwLockRecursiveTimed extension traits have been removed from lock_api. (#533)」;提交 8150554「Add dedicated recursive reader-writer lock」。
0.12.5 的「递归读」是按调用选的。同一把 RwLock 上,read() 遇到等待中的写者会让路,read_recursive() 不让(src/raw_rwlock.rs 第 508–520 行):
fn try_lock_shared_fast(&self, recursive: bool) -> bool {
let state = self.state.load(Ordering::Relaxed);
// We can't allow grabbing a shared lock if there is a writer, even if
// the writer is still waiting for the remaining readers to exit.
if state & WRITER_BIT != 0 {
// To allow recursive locks, we make an exception and allow readers
// to skip ahead of a pending writer to avoid deadlocking, at the
// cost of breaking the fairness guarantees.
if !recursive || state & READERS_MASK == 0 {
return false;
}
}
外壳上的入口是 lock_api::RwLock 的 read_recursive / try_read_recursive / read_arc_recursive 等(lock_api/src/rwlock.rs 第 879–1036 行)。
0.13.0 改成按类型选:RawRwLock<const RECURSIVE: bool = false>,RawRwLockRecursive = RawRwLock<true>,RecursiveRwLock<T> = lock_api::RwLock<RawRwLockRecursive, T>。文档(src/raw_rwlock.rs 第 48–58 行):
If
RECURSIVEistrue, readers are prioritized over writers: acquiring a read lock is guaranteed not to block unless a writer currently holds the lock. This permits recursive read locking, but may starve writers.
判断条件和 0.12.5 的递归分支相同(!RECURSIVE || state & READERS_MASK == 0),区别在作用范围:
0.12.5 RwLock + read_recursive | 0.13.0 RecursiveRwLock | |
|---|---|---|
| 读者优先作用于 | 只有调用 read_recursive 的那几处 | 这把锁上的所有 read() |
同一把锁上的普通 read() | 仍给等待中的写者让路 | 不存在「普通」读,全部读者优先 |
| 写者饿死的风险 | 只来自那几处递归读 | 来自所有读者 |
RwLock 上的 read_recursive | 有 | 没有,编译失败 |
升级时,原来只在一两个调用点写了 read_recursive 的代码,要么把整把锁的类型换成 RecursiveRwLock(接受所有读者都优先),要么改掉重入读本身(读、写与写者优先;本卷的常见坑汇总在 09 常见坑)。
#533 修复清单:与本卷正文相关的四项
下面四项都在 #533 中,CHANGELOG 各有一行。每项给出 0.12.5 的代码位置和代码层面的差别。
unlock_upgradable_fair 不交接
CHANGELOG:「Fixed unlock_upgradable_fair not forcing a fair handoff. (#533)」;提交 335d596「Fix fair upgradable rwlock unlocking」,只改了一个参数。
0.12.5 的 RawRwLockUpgradeFair::unlock_upgradable_fair(src/raw_rwlock.rs 第 396–417 行)在有人停车时调用的是 self.unlock_upgradable_slow(false)(第 416 行),参数 force_fair 是 false,和非公平的 unlock_upgradable(第 362 行)完全相同。于是公平解锁只在 eventual fairness 的计时器恰好到期时才交接。提交把它改成 true。RwLockUpgradableReadGuard::unlock_fair 走的就是这个函数。
条件等待超时后不复查谓词
CHANGELOG:「Fixed conditional Condvar waits reporting a timeout without rechecking the predicate. (#533)」;提交 5fd0ddb「Recheck condvar predicates after timeouts」。
0.12.5 的 wait_while_until_internal(src/condvar.rs 第 391–409 行):
let mut result = WaitTimeoutResult(false);
while !result.timed_out() && condition(mutex_guard.deref_mut()) {
result =
self.wait_until_internal(unsafe { MutexGuard::mutex(mutex_guard).raw() }, timeout);
}
result
最后一次 wait_until_internal 超时返回时,循环条件里 !result.timed_out() 先为假,condition 不再被调用,函数返回「超时」。而超时返回时已经重新拿到了锁,这时谓词可能已经变成假(别的线程在超时前后改了条件)。调用方看到 timed_out() == true,会以为条件没满足。0.13.0 的 wait_while_until(lock_api/src/condvar.rs 第 388–416 行)每轮先查谓词、为假就返回「未超时」,然后才看上一轮是否超时。所以在 0.12.5 上,wait_while_for 返回超时后最好再看一眼条件本身。Condvar 的整体语义见 Condvar。
generic parker 忙等
CHANGELOG:「Fixed the generic thread parker continuously busy-spinning while parked. (#533)」;提交 4a3656f「Yield in generic thread parker」。
generic 后端用在没有专门实现的平台上。0.12.5 的 park(core/src/thread_parker/generic.rs 第 43–59 行):
unsafe fn park(&self) {
while self.parked.load(Ordering::Acquire) != false {
spin_loop();
}
}
停车的线程一直在 CPU 上转,park_until 同样只是边转边看时钟。0.13.0 在循环里改调 thread_yield()。本机和生产服务器都不走这个后端(ThreadParker 的各平台实现)。
Apple 上超时用墙上时钟
CHANGELOG:「Made timed waits on Apple platforms independent of wall-clock changes. (#533)」;提交 c175220「Use relative timed waits on Apple platforms」。
0.12.5 在 Apple 平台上把相对超时换算成 gettimeofday 的绝对时间,再交给 pthread_cond_timedwait(core/src/thread_parker/unix.rs 第 185–198 行):
// Returns the current time on the clock used by pthread_cond_t as a timespec.
#[cfg(target_vendor = "apple")]
#[inline]
fn timespec_now() -> libc::timespec {
let mut now = MaybeUninit::<libc::timeval>::uninit();
let r = unsafe { libc::gettimeofday(now.as_mut_ptr(), ptr::null_mut()) };
// ...
}
墙上时钟往回拨时,try_lock_for、Condvar::wait_for 这类等待会多等回拨的那段时间;往前拨不会提前结束,因为等待循环每次醒来都用单调时钟复查(见 ThreadParker 与 超时按墙上时钟换算,本卷没有实测,不改本机时间)。0.13.0 在 Apple 上改用 pthread_cond_timedwait_relative_np,直接传相对时间(0.13.0 的 unix.rs 第 207–209 行 的注释)。本机 macOS 正好在这个分支上。
#533 及其他 PR 中的其余修复
下面这些按上游的 CHANGELOG 或提交标题原文列出,附短哈希,本卷不展开、不加说明。
涉及守卫的 Send / Sync 约束
| CHANGELOG 原文 | 提交 |
|---|---|
「Corrected the Send and Sync bounds of lock guards in lock_api. (#533)」 | cb6665f「Derive lock guard auto traits from fields」 |
涉及死锁检测
| CHANGELOG 原文 | 提交 |
|---|---|
「Corrected RwLock deadlock tracking to avoid false positives and missed cycles, and prevented dependency feature unification from enabling tracking when only parking_lot_core/deadlock_detection is enabled. (#533)」 | 6693985「Fix deadlock detection ownership and reporting」 |
「Added Once deadlock tracking and made overlapping wait cycles report as a single component. (#533)」 | 同上 |
涉及内存序
| 出处 | 原文 |
|---|---|
| #533 中的提交 | 5fa2020「Fix rwlock upgrade memory orderings」 |
| #533 中的提交 | 874555e「Document raw lock synchronization requirements」 |
其他
| 出处 | 原文 |
|---|---|
| #533 中的提交 | fb08e42「Fix WordLock queue unlock retries」 |
| #499 | fbfe854「word_lock: use AtomicPtr (with polyfill for operations missing on old Rust)」 |
| #533 中的提交 | 39a8778「Construct unpark handles before releasing parkers」 |
| #533 中的提交 | 8d7254a「Validate thread parker results in release builds」 |
| #533 中的提交 | c7d7902「Handle oversized timeouts consistently」 |
| #533 中的提交 | 39ee223「Clarify aborted requeue results」 |
| CHANGELOG(#533) | 「Fixed timed waits on ESP-IDF using the wrong clock.」(3a21d7f) |
| CHANGELOG(#533) | 「Fixed the futex timeout ABI on 32-bit Linux targets, including time64-only architectures, with fallback to time32 when the kernel lacks time64 support.」(43d5007) |
| CHANGELOG(#538) | 「Fixed parking on WASI by using the pthread backend when the nightly atomic intrinsics are unavailable, including on stable threaded WASI targets.」 |
| CHANGELOG(#549) | 「Fixed compilation on L4Re.」(696f28b) |
| #539 | 94d1070「fix: pass integer pointers to futex syscalls」 |
| #540 | 7d52c89「core: fix SpinWait spin counts starting at 2 instead of 1」(SpinWait) |
WordLock 的结构见 WordLock:停车场自己的锁。
小结
- 最终公平去掉后,「少数线程、长临界区、放锁即重新加锁」会出现长时间独占(E3:2 线程持锁 100 µs 时一轮里 99 563 / 100 000 次被同一线程连续拿走;持锁 1 ms、10 ms 时 0.12.5 严格交替、0.13.0 每轮 100 / 0);用
bump、unlock_fair或FairMutex显式选择公平。 hardware-lock-elision只在 x86 上生效,本机从未用到;删除无替代,写着这个 feature 的依赖会解析失败。read_recursive从「按调用」变成「按类型」:RecursiveRwLock上所有读者都优先,写者饿死的风险随之扩大到整把锁。- #533 中与正文相关的四项修复:
unlock_upgradable_fair传了false(第 416 行)、wait_while_until_internal超时后不再查谓词(第 403 行)、generic parker 纯自旋(第 45–47 行)、Apple 上用gettimeofday算超时(第 186–198 行)。 - 涉及守卫
Send/Sync、死锁检测和内存序的修复,本页只列上游原文和短哈希。