Rust in Everything
基于 parking_lot 0.12.5

可升级读、降级与 with_upgraded

主线位置:主线 1 在读写锁上的一个分支。「先读、看情况再写」时,可升级读者先以读者身份进来(和普通读者共存),决定要写时 upgrade 在状态字上一步换成 WRITER_BIT,然后走写锁的第二步 wait_for_readers。它和普通写锁共用 读、写与写者优先里的全部机制。

stateDiagram-v2
  direction LR
  R: 读锁(ONE_READER)
  U: 可升级读(ONE_READER 加 UPGRADABLE_BIT)
  W: 写锁(WRITER_BIT)
  U --> W: upgrade、try_upgrade、with_upgraded 进入
  W --> U: downgrade_to_upgradable、with_upgraded 退出
  W --> R: downgrade
  U --> R: downgrade(可升级读守卫的)

为什么要第三种锁

两个读者都想「读完再升级成写」,如果读锁可以直接升级,两个都会在升级时等对方的读锁释放:死锁。可升级读锁把「将来可能要写」的读者限制为同一时刻最多一个,于是升级时等的只是普通读者,它们迟早会走。lock_api 的 trait 文档原话:「There may only be one upgradable lock at any time, otherwise deadlocks could occur when upgrading」(lock_api/src/rwlock.rs:213-219)。

加锁与解锁

try_lock_upgradable_fast:

fn try_lock_upgradable_fast(&self) -> bool {
    let state = self.state.load(Ordering::Relaxed);

    // We can't grab an upgradable lock if there is already a writer or
    // upgradable reader.
    if state & (WRITER_BIT | UPGRADABLE_BIT) != 0 {
        return false;
    }

    if let Some(new_state) = state.checked_add(ONE_READER | UPGRADABLE_BIT) {
        self.state
            .compare_exchange_weak(state, new_state, Ordering::Acquire, Ordering::Relaxed)
            .is_ok()
    } else {
        false
    }
}

和普通读者一样计一个读者,再加一个 UPGRADABLE_BIT。被挡住的条件是 WRITER_BIT | UPGRADABLE_BIT:有写者(持有或在等读者)、或已有一个可升级读者。慢路径是 lock_common(.., TOKEN_UPGRADABLE, .., WRITER_BIT | UPGRADABLE_BIT)(744-781 行)。反过来,可升级读者在场时,普通读者照常进出(读锁只看 WRITER_BIT),写者则进不来(写锁的第一步也看 UPGRADABLE_BIT)。

解锁(343-363 行):没有 PARKED_BIT 就一次 CAS 减掉 ONE_READER | UPGRADABLE_BIT;有人停车就进 unlock_upgradable_slow,用 wake_parked_threads(0, ..) 叫醒主队列里的人。和读锁不同,可升级读锁的解锁要负责叫醒主队列:在它持有期间被挡住的写者和可升级读者都排在主队列里。

upgrade:一步换成 WRITER_BIT

365-375 行:

unsafe fn upgrade(&self) {
    let state = self.state.fetch_sub(
        (ONE_READER | UPGRADABLE_BIT) - WRITER_BIT,
        Ordering::Acquire,
    );
    if state & READERS_MASK != ONE_READER {
        let result = self.upgrade_slow(None);
        debug_assert!(result);
    }
}

一次 fetch_sub 同时做三件事:读者数减一(自己不再算读者)、去掉 UPGRADABLE_BIT、加上 WRITER_BIT(减一个负数)。之后的状态正是「写者已占 WRITER_BIT、等剩下的读者」,新读者从这一刻起进不来。如果自己是唯一的读者,升级立即完成;否则 upgrade_slow 调 wait_for_readers(timeout, ONE_READER | UPGRADABLE_BIT)(871-877 行)——第二个参数是超时回滚时要恢复成的值。

为什么这一步不会和别人冲突:WRITER_BIT 和 UPGRADABLE_BIT 互斥,持有 UPGRADABLE_BIT 的线程知道此刻没有任何写者占着 WRITER_BIT,可以无条件地把自己的位换过去。

方法做法失败时
upgradefetch_sub 换位,有其他读者就 wait_for_readers 等到底不会失败
try_upgrade状态恰好是 ONE_READER | UPGRADABLE_BIT 时一次 CAS 换成 WRITER_BIT;否则 try_upgrade_slow 在「读者只剩自己」时 CAS(保留其余标志位)还有其他读者就返回 false,什么也不改
try_upgrade_for / _until同 upgrade 先换位,再带超时 wait_for_readers超时把 WRITER_BIT 换回 ONE_READER | UPGRADABLE_BIT,叫醒被挡住的读者,返回 false

源码位置:try_upgrade 377-393 行 与 852-869 行,带超时的两个在 479-503 行,超时回滚在 wait_for_readers。

注意 try_upgrade 与 try_upgrade_for(Duration::ZERO) 不一样:前者在有其他读者时不改状态;后者先占了 WRITER_BIT(这一瞬间挡住了新读者),超时后再还回去。

三种降级

方法状态字变化有 PARKED_BIT 时叫醒
写 → 读 downgradefetch_add(ONE_READER - WRITER_BIT)wake_parked_threads(ONE_READER, ..):队头所有读者,至多一个可升级读者或写者
写 → 可升级读 downgrade_to_upgradablefetch_add((ONE_READER | UPGRADABLE_BIT) - WRITER_BIT)wake_parked_threads(ONE_READER | UPGRADABLE_BIT, ..):只叫醒读者,写者和可升级读者被 Skip
可升级读 → 读 downgrade_upgradablefetch_sub(UPGRADABLE_BIT)wake_parked_threads(ONE_READER, ..):队头所有读者,至多一个可升级读者或写者

源码在 189-201 行、427-450 行、879-909 行。三个都是一次原子操作完成,中间没有「放开」的瞬间,所以 lock_api 文档说降级「without allowing any thread to take an exclusive lock in the meantime」。

wake_parked_threads 的第一个参数是累加器的初值,也就是「降级之后锁里已经有什么」。downgrade_to_upgradable 从 ONE_READER | UPGRADABLE_BIT 起算,过滤函数看到累加器里已有 UPGRADABLE_BIT,就跳过所有写者和可升级读者——锁里已经有一个可升级读者了。另外两种降级从 ONE_READER 起算,可能叫醒一个写者;那个写者回去抢 WRITER_BIT(降级后有读者也能抢到),然后等这批读者走光。

三个降级的回调都返回 TOKEN_NORMAL,没有交接:被叫醒的线程自己去抢。

lock_api 的 trait 拆分

RwLock 的原始锁能力被切成十个 unsafe trait(lock_api/src/rwlock.rs:38-310),外壳 RwLock<R, T> 按 R 实现了哪些来决定提供哪些方法。和可升级读有关的五个:

trait方法守卫上对应的 API
RawRwLockDowngradedowngradeRwLockWriteGuard::downgrade
RawRwLockUpgradelock_upgradable、try_lock_upgradable、unlock_upgradable、upgrade、try_upgradeupgradable_read、try_upgradable_read、RwLockUpgradableReadGuard::upgrade / try_upgrade,以及 with_upgraded / try_with_upgraded
RawRwLockUpgradeFairunlock_upgradable_fair、bump_upgradableRwLockUpgradableReadGuard::unlock_fair / unlocked_fair / bump
RawRwLockUpgradeDowngradedowngrade_upgradable、downgrade_to_upgradableRwLockUpgradableReadGuard::downgrade、RwLockWriteGuard::downgrade_to_upgradable
RawRwLockUpgradeTimedtry_lock_upgradable_for / _until、try_upgrade_for / _untiltry_upgradable_read_for 等,以及 try_with_upgraded_for / _until

parking_lot 的 RawRwLock 十个 trait 全都实现了。另外五个是 RawRwLock 本身、Fair、Timed、Recursive、RecursiveTimed,见 raw trait 与契约。

两处与「公平」相关的细节照代码记下:

  • unlock_upgradable_fair(396-417 行)调用的是 unlock_upgradable_slow(false),和普通的 unlock_upgradable 一样,没有强制交接;bump_upgradable 走的 bump_upgradable_slow 才传 true(924-929 行)。0.13 的 CHANGELOG 记为「Fixed unlock_upgradable_fair not forcing a fair handoff. (#533)」,见 去掉的与修正的。
  • 可升级读者在 unlock_upgradable_slow 的公平分支里,把要叫醒的线程的 token 加到「当前状态减去自己」上,加法溢出就退回非公平分支(805-808 行),理由是回调里不能 panic。

with_upgraded:借一下写锁

lock_api/src/rwlock.rs:2196-2219:

pub fn with_upgraded<Ret, F: FnOnce(&mut T) -> Ret>(&mut self, f: F) -> Ret {
    unsafe {
        self.rwlock.raw.upgrade();
    }

    // Safety: We just upgraded the lock, so we have mutable access to the data.
    // This will restore the state the lock was in at the start of the function.
    defer!(unsafe { self.rwlock.raw.downgrade_to_upgradable() });

    // Safety: We upgraded the lock, so we have mutable access to the data.
    // When this function returns, whether by drop or panic,
    // the drop guard will downgrade it back to an upgradeable lock.
    f(unsafe { &mut *self.rwlock.data.get() })
}

upgrade 得到 &mut T,f 结束(正常返回或 panic)时 defer! 降回可升级读。和「upgrade 拿到写守卫、用完 downgrade_to_upgradable」等价,好处是只借用守卫(&mut self),不用把守卫按值传来传去。它是守卫上少有的写成方法(而非关联函数)的 API。

历史:lock_api 0.4.10 加入(CHANGELOG「Add with_upgraded API to upgradable read locks (#386)」),0.4.11 修过一次(「Fixed RwLockUpgradeableReadGuard::with_upgraded. (#393)」),0.4.12 又修了 Arc 版本连调两次时的 panic(「Fixed panic when calling with_upgraded twice on a ArcRwLockUpgradableReadGuard (#431)」)。

自锁的几种写法(实测)

可升级读锁只有一个名额,同一线程很容易把自己挡住。exp-parking_lot/pitfalls-upgradable(功能实验,不计时;2 秒看门狗),每种写法跑 3 次:

写法结果原因
A 持有可升级读 200 ms;B 申请可升级读,C 申请普通读B 等了 206–210 ms,C 立即拿到(41–83 ns)UPGRADABLE_BIT 只挡可升级读者和写者,不挡普通读者
同一线程:upgradable_read() 后再 upgradable_read()3/3 卡死第二次被自己的 UPGRADABLE_BIT 挡住
同一线程:upgradable_read() 后再 write()3/3 卡死写锁第一步要求 UPGRADABLE_BIT 为 0
同一线程:read(),再 upgradable_read(),再 upgrade()3/3 卡死(前两步都成功,卡在 upgrade)upgrade 等其余读者走光,其中一个是自己的读守卫
反向:只持有可升级读,upgrade()3/3 正常读者只有自己,fetch_sub 后立即完成
反向:with_upgraded 改值后,别的线程在可升级读仍被持有时 read()3/3 正常,读到改后的值with_upgraded 结束时已降回可升级读,普通读者不受影响

第一行说明「两个线程都用 upgradable_read」会互相排队,并发度和用 Mutex 差不多;可升级读适合「多数读者用 read(),只有一个线程(或少数时刻)需要读后写」的场景。要改的地方:需要写的线程直接 write();或者先 read() 判断,放掉读锁再 write(),在写锁里重新检查条件。

小结

  • 可升级读 = 读者计数加一,再加 UPGRADABLE_BIT;与普通读者共存,与写者和其他可升级读者互斥。它的解锁要负责叫醒主队列。
  • upgrade 一次 fetch_sub 把自己从「读者 + UPGRADABLE_BIT」换成 WRITER_BIT,然后和写锁一样 wait_for_readers;try_upgrade 有其他读者就失败且不改状态;带超时的版本超时后回滚。
  • 三种降级都是一次原子操作,wake_parked_threads 的初值决定叫醒谁:降成可升级读时只叫醒读者。
  • lock_api 用五个 trait 描述升级 / 降级能力,parking_lot 全部实现;0.12.5 的 unlock_upgradable_fair 没有强制交接。
  • with_upgraded = upgrade + defer!(downgrade_to_upgradable),0.4.10 加入、修过两次。
  • 实测:同一线程持有可升级读再申请可升级读或写锁、或同时持有读锁和可升级读再升级,都会自锁。

💬 关联讨论

发起讨论
加载中...