Rust in Everything
基于 parking_lot 0.12.5

两个线程都用 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() 并重新检查。

💬 关联讨论

发起讨论
加载中...