两个线程都用 upgradable_read,结果一个等另一个;同一线程再拿就卡死
主线位置:主线 1 的读写锁版本。可升级读 = 读者计数加一,再加
UPGRADABLE_BIT;UPGRADABLE_BIT挡住其他可升级读者和写者,不挡普通读者;upgrade把自己换成WRITER_BIT后等其余读者走光(可升级读、降级与 with_upgraded)。
flowchart TB u1["线程 A:upgradable_read()"] --> bit["UPGRADABLE_BIT 置位"] bit -->|"线程 B:upgradable_read()"| q["B 排队,等 A 放开"] bit -->|"线程 C:read()"| r["C 立即拿到"] bit -->|"A 自己:upgradable_read() 或 write()"| s1["A 等 A:卡死"] rd["A 先 read(),再 upgradable_read()"] --> up["upgrade():等其余读者走光"] up --> s2["其余读者里有 A 自己的读守卫:卡死"]
现象
- 把「先读、可能要改」的代码统一改成
upgradable_read(),想着读者之间还能并行,结果吞吐和Mutex差不多。 - 一个持有可升级读守卫的函数,调用了另一个也拿可升级读(或写锁)的函数,线程卡住。
最小复现
exp-parking_lot/pitfalls-upgradable(功能实验,2 秒看门狗),每种 3 次:
| 写法 | 结果 |
|---|---|
| A 持可升级读 200 ms;B 申请可升级读,C 申请普通读 | B 等了 206–210 ms,C 立即拿到(41–83 ns) |
同一线程:upgradable_read() 后再 upgradable_read() | 3/3 卡死 |
同一线程:upgradable_read() 后再 write() | 3/3 卡死 |
同一线程:read(),再 upgradable_read(),再 upgrade() | 3/3 卡死(前两步都成功,卡在 upgrade) |
反向:只持有可升级读,upgrade() | 3/3 正常 |
反向:with_upgraded 改值后,别的线程在可升级读仍被持有时 read() | 3/3 正常,读到改后的值 |
let _r = lock.read();
let u = lock.upgradable_read(); // 成功:普通读者不挡可升级读
let _w = RwLockUpgradableReadGuard::upgrade(u); // 卡死:等的读者里有 _r
源码出处
- 互斥规则:
try_lock_upgradable_fast「We can't grab an upgradable lock if there is already a writer or upgradable reader」,state & (WRITER_BIT | UPGRADABLE_BIT) != 0就失败;写锁的慢路径同样要求这两位为 0(613-616 行)。lock_api 的 trait 文档写明了原因:「There may only be one upgradable lock at any time, otherwise deadlocks could occur when upgrading」(lock_api/src/rwlock.rs:214-219)。 upgrade(366-375 行)一次fetch_sub把ONE_READER | UPGRADABLE_BIT换成WRITER_BIT,读者数不只自己时进upgrade_slow→wait_for_readers。它不知道其余读者里有没有本线程的守卫。
「同一时刻只有一个可升级读者」是设计:如果允许两个,它们同时 upgrade 就会互等对方的读者身份退出。代价就是可升级读者之间串行。
怎么改
多数读者用
read(),只有真正可能要写的那条路径用可升级读;或者干脆:先读、放锁、再写、写锁里重新检查条件:
读阶段完全并行,代价是条件要检查两次。let need = { lock.read().needs_update() }; if need { let mut w = lock.write(); if w.needs_update() { // 放锁期间可能已被别人改过 w.update(); } }同一线程已经持有任何守卫时,不要再拿可升级读或写锁;要在可升级读里写,用
with_upgraded(|v| ..)或RwLockUpgradableReadGuard::upgrade(u),并确认本线程没有别的读守卫活着。try_upgrade在还有其他读者时立即返回失败、不改状态,可以用来探测而不卡住。
小结
- 可升级读同一时刻只有一个,可升级读者之间完全串行;普通读者不受影响。
- 同一线程持有可升级读再拿可升级读或写锁、或同时持有读锁和可升级读再
upgrade,都会自锁(实测 3/3)。 - 设计原因是避免两个可升级读者同时升级互等。
- 改法:读路径用
read(),要写时放锁再write()并重新检查。
💬 关联讨论
发起讨论加载中...