可升级读、降级与 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)。
加锁与解锁
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
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,可以无条件地把自己的位换过去。
| 方法 | 做法 | 失败时 |
|---|---|---|
upgrade | fetch_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 时叫醒 |
|---|---|---|
写 → 读 downgrade | fetch_add(ONE_READER - WRITER_BIT) | wake_parked_threads(ONE_READER, ..):队头所有读者,至多一个可升级读者或写者 |
写 → 可升级读 downgrade_to_upgradable | fetch_add((ONE_READER | UPGRADABLE_BIT) - WRITER_BIT) | wake_parked_threads(ONE_READER | UPGRADABLE_BIT, ..):只叫醒读者,写者和可升级读者被 Skip |
可升级读 → 读 downgrade_upgradable | fetch_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 |
|---|---|---|
RawRwLockDowngrade | downgrade | RwLockWriteGuard::downgrade |
RawRwLockUpgrade | lock_upgradable、try_lock_upgradable、unlock_upgradable、upgrade、try_upgrade | upgradable_read、try_upgradable_read、RwLockUpgradableReadGuard::upgrade / try_upgrade,以及 with_upgraded / try_with_upgraded |
RawRwLockUpgradeFair | unlock_upgradable_fair、bump_upgradable | RwLockUpgradableReadGuard::unlock_fair / unlocked_fair / bump |
RawRwLockUpgradeDowngrade | downgrade_upgradable、downgrade_to_upgradable | RwLockUpgradableReadGuard::downgrade、RwLockWriteGuard::downgrade_to_upgradable |
RawRwLockUpgradeTimed | try_lock_upgradable_for / _until、try_upgrade_for / _until | try_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 记为「Fixedunlock_upgradable_fairnot 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 加入、修过两次。- 实测:同一线程持有可升级读再申请可升级读或写锁、或同时持有读锁和可升级读再升级,都会自锁。