Rust in Everything
基于 parking_lot 0.12.5

同一线程第二次 read() 卡住,只在有写者排队时

主线位置:主线 1 的读写锁版本。写者先抢 WRITER_BIT、再等读者走光;新读者看见 WRITER_BIT 就排队(读、写与写者优先)。同一个线程既是「还没走的读者」,又是「排在写者后面的新读者」,两头互等。

sequenceDiagram
  participant A as 线程 A
  participant S as 状态字
  participant W as 写者 W
  A->>S: read():读者数 1
  W->>S: write():抢到 WRITER_BIT,读者数不为 0
  W->>W: wait_for_readers:在 addr+1 停车
  A->>S: 再 read():try_lock_shared_fast 看到 WRITER_BIT,失败
  A->>A: 在 addr 停车,等 W 解锁
  Note over A,W: W 等 A 的第一个读锁,A 等 W:谁也不动

现象

一段代码平时好好的,压测或线上偶尔整个线程卡住,CPU 为零,没有 panic、没有日志。栈上能看到同一个线程持有一个 RwLockReadGuard,又在 read() 里睡着。没有写者时永远复现不出来。

最小复现

E5(exp-parking_lot/recursive-read,不开 deadlock_detection):A read() 后睡 100 ms,B 在 30 ms 时 write(),A 醒来再 read();2 秒看门狗。

let _a = lock.read();
thread::sleep(Duration::from_millis(100)); // 期间 B 调 write()
let _b = lock.read();                       // 卡在这里
模式结果(各 3 次)
parking_lot read(),有写者3/3 卡死,看门狗触发
std RwLock::read(),有写者(本机 queue.rs)3/3 卡死
parking_lot read_recursive(),有写者3/3 正常
parking_lot read(),没有写者(反向验证)3/3 正常

最后一行说明问题只在「中间插进一个写者」时出现,所以单元测试几乎抓不到。

源码出处

try_lock_shared_fast:

// 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;
    }
}

WRITER_BIT 在读者数不为 0 时的含义是「有写者已经占位、在等读者退出」。普通 read() 传 recursive = false,一律排队,这正是写者不被读者流饿死的机制。文档写明了后果(src/rwlock.rs:17-21,lock_api 的 read 文档 rwlock.rs:446-451 也写了一遍):「attempts to recursively acquire a read lock within a single thread may result in a deadlock」。

std 不承诺优先策略,但本机 std 用的 queue.rs 也是写者优先,所以同样卡死(和 std RwLock 比)。

本站其他卷遇到的同一件事:tokio watch::Receiver::borrow() 拿着的就是这种同步读锁,文档提醒「在同一线程上可能死锁」(tokio 卷 watch);dioxus 的 SyncStorage 格子是 parking_lot::RwLock(dioxus 卷 generational-box)。

怎么改

  • 不要在持有读守卫时调用可能再读同一把锁的函数。最常见的是读守卫活过了一次回调、一次 ? 返回前的另一次查询。先把需要的数据复制出来,放掉守卫再继续。
  • 确实需要重入读时用 read_recursive():读者数不为 0 时越过等待中的写者。代价是写者可能等很久:E4 的反向组里,11 个读者改用 read_recursive 后写者等待 p50 从 5.9 µs 涨到约 280 µs,最大 52 ms(和 std RwLock 比)。
  • 0.13 删掉了 read_recursive,改成单独的 RecursiveRwLock 类型(整把锁读者优先),升级时见 去掉的与修正的。
  • 定位:macOS 上 sample 一下卡住的进程,栈顶是 __psynch_cvwait,往下一帧是 parking_lot 的慢路径(ThreadParker 的各平台实现);再往下找到本线程更早的那次 read()。

小结

  • 同线程重复 read() 只在中间有写者占了 WRITER_BIT 时死锁,平时测不出来。
  • 来源是写者优先:try_lock_shared_fast 看见 WRITER_BIT 就让新读者排队。
  • E5:parking_lot 与本机 std 都 3/3 卡死;read_recursive 和「没有写者」都正常。
  • 改法:读守卫别跨过可能重入的调用;必须重入就用 read_recursive,接受写者变慢。

💬 关联讨论

发起讨论
加载中...