同一线程第二次 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 正常 |
最后一行说明问题只在「中间插进一个写者」时出现,所以单元测试几乎抓不到。
源码出处
// 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,接受写者变慢。