Rust in Everything
基于 parking_lot 0.13.0

去掉的与修正的

主线位置:不在主线上。这一页是升级清单: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_HANDOFFsrc/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 µs100 00095 / 95 / 3668 388 / 21 950 / 37 10817 048 / 17 753 / 19 805
100 µs100 00042 / 17 / 1013 974 / 11 217 / 10 36253 351 / 99 563 / 83 217
1 ms2 0001 / 1 / 12 000 / 1 545 / 1 7782 000 / 2 000 / 2 000
10 ms3001 / 1 / 1299 / 300 / 268300 / 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 RECURSIVE is true, 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_recursive0.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」
#499fbfe854「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)
#53994d1070「fix: pass integer pointers to futex syscalls」
#5407d52c89「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、死锁检测和内存序的修复,本页只列上游原文和短哈希。

💬 关联讨论

发起讨论
加载中...