Rust in Everything
基于 parking_lot 0.12.5

开了 deadlock_detection,程序卡死了却什么也没报

主线位置:主线 4 的一条支线。死锁检测在每次加解锁时记账,check_deadlock() 再逐桶建「资源 → 持有线程、线程 → 等待的 key」的图找环(死锁检测)。图里只有「正在停车、不带超时」的线程和记过账的锁。

flowchart TB
  hang["程序卡住"] --> poll{"有线程在轮询 check_deadlock()?"}
  poll -->|"否"| none1["永远不会报告"]
  poll -->|"是"| kind{"卡在什么上?"}
  kind -->|"两把以上 parking_lot 锁成环,都不带超时"| hit["约 65 ms 报告,线程随后永久停车"]
  kind -->|"环上有 try_lock_for / wait_for"| miss1["看不到"]
  kind -->|"std 锁或别的库的锁"| miss2["看不到"]
  kind -->|"Condvar::wait 等不到通知"| miss3["看不到"]
  kind -->|"等待链没闭合(等一个卡在 IO 上的线程)"| miss4["不是死锁,不报"]

现象

为了查一次卡死,在 Cargo.toml 里给 parking_lot 打开了 deadlock_detection,重新编译、复现,进程还是安安静静地卡着,没有任何输出。

复现

E8(exp-parking_lot/deadlock-detect,--features dd,后台每 50 ms 调一次 check_deadlock(),2 秒内看是否报告),各 3 次:

模式构造结果
abba两线程各持一把 Mutex 再拿对方的65–66 ms 报告 1 个环、2 个线程
ring3三线程 A→B、B→C、C→A62–67 ms 报告 1 个环、3 个线程
self一个线程对同一把 Mutex 加两次60–61 ms 报告 1 个环、1 个线程
timedabba,第二把锁用 try_lock_for(5 s)2 秒内无报告
stdabba,换成两把 std::sync::Mutex2 秒内无报告
chain3(反向)A→B、B→C,C 持锁 1 秒后放开,没有环无报告
none(反向)两线程依次加两把锁,不交叉无报告

报告之后再调一次 check_deadlock(),三种能检测到的模式都返回 0 个:被报告的线程已经被标记、永久停车,不会再进图。

源码出处

  • 不会自己跑:模块文档的例子就是「Create a background thread which checks for deadlocks every 10s」(src/deadlock.rs),检测函数只是 pub use parking_lot_core::deadlock::check_deadlock。
  • 带超时的等待被排除:停车时记下 parked_with_timeout(core/src/parking_lot.rs:612),建图时跳过这样的线程(1268 行、1332 行),环因此断了一环。
  • std 锁不经过停车场,也不调 acquire_resource,图里没有它们。
  • Condvar::wait 等的 key 是 condvar 的地址,没有线程「持有」它,图上没有回到等待者的边。
  • 被报告的线程在 on_unpark 里抓自己的栈发回,然后不再返回(死锁检测):检测只负责报告,不负责恢复。

怎么改

  • 把它当作「锁顺序成环」的探测器,不是通用的卡死诊断。适合在测试或预发环境里开着,跑压力测试,发现两把以上 parking_lot 锁的加锁顺序问题。
  • 一定要起轮询线程,并在拿到报告后打印 thread_id() 和 backtrace(),然后让进程退出:这些线程不会再动了。
  • 其他卡死用别的工具:macOS 上 sample <pid> 看所有线程的栈(卡在 parking_lot 锁上的线程栈顶是 __psynch_cvwait,下一帧是 lock_slow);Linux 上 gdb -p 或 eu-stack。
  • 不要在生产构建里开:它让单线程每对加解锁从 5.28 ns 变成 7.05 ns,并让每个加过锁的线程都创建 ThreadData、推动哈希表扩容(E7、E8);还和 send_guard 互斥(send_guard 与 deadlock_detection 冲突)。

小结

  • check_deadlock() 要自己开线程轮询,不轮询就永远不报。
  • 盲区:环上有带超时的等待、std 或其他库的锁、Condvar 等待、自旋中还没停车的线程;不闭合的等待链本来就不是死锁。
  • E8:两线程、三线程环和同线程重入都在 60–67 ms 内报告一次;timed、std 2 秒内无报告。
  • 被报告的线程永久停车,检测只用于调试,不用于恢复。

💬 关联讨论

发起讨论
加载中...