现有代码中的死锁检测
本文简要讨论了死锁的行为,并提出了一种简单的检测方法。
引言
死锁是多线程编程中常见的问题。在多线程开发中,开发人员最常遇到的问题是临界区。使用多个锁并不罕见,但如果不注意锁的顺序,或者注意调用它们的上下文(例如,在回调函数中),就会形成死锁。(除了明显的临界区之外,还有很多原因会导致死锁,例如,两个线程互相等待对方发出信号,但我们在这里不讨论它们)。
与任何与线程相关的东西一样,时机就是一切。最棘手的死锁是那些很少发生的死锁,它们往往发生在客户现场……
如果我们能让罕见的情况变成常见的情况呢? 回想一下,死锁不发生的原因在于,可能发生死锁的两个线程没有同时出现在有问题的区域。所以,我们所要做的就是记录它们在这些有问题区域的“访问”记录,然后我们需要验证锁的顺序始终是相同的,如果不相同,就输出堆栈跟踪并通知开发人员我们发现了锁顺序的不匹配。
附带的 ZIP 文件包含了一个执行此操作的 DLL。该 DLL 可以拦截所有常用的(Enter
、Exit
、TryEnter
方法,但如果使用得当,也可以轻松扩展以支持其他方法),包括 .NET 的 `lock` 关键字,并跟踪锁的顺序。一旦发现顺序有问题,它会生成两个堆栈跟踪,并将您引导到有问题的锁的示例(修复错误后,请重复测试,并查看在另一个流程中是否有其他有问题的锁)。
请注意,死锁不必真正发生;重要的是,可疑的流程(或所有流程)至少被执行一次。
使用代码
- 将文件 incslock.cs 添加到您的项目中
- 添加对 slockimp.dll 的引用
- 编译您的组件并执行
分析堆栈(基于 slockimp)
- 检测到问题后,控制台(如果存在)将输出最后与堆栈中的第 n 个元素冲突的锁
- 在工作目录中创建两个文件:first_xxx.txt 和 now_yyy(xxx 和 yyy 代表数字)
- 转到“now”文件 – 找到最后一个锁(在 DLL 内部的最后四个调用之前)
- 为了找到另一个有问题的锁,您可以
- 从堆栈的开头找到第 n 个锁(不计算递归锁定的锁以及已锁定和释放的锁)
- 查找“first”文件中的最后一个锁
- 遍历“first”文件的堆栈,找到锁定来自 3.a 的锁的位置
这就是导致问题的锁
这是实现的第二个版本,它现在支持更复杂的场景,如哲学家就餐问题(感谢下面的 Sergey 的问题)。堆栈文件编号如下:0_xxx.txt、1_xxx.txt 等等…
0_xxx 指向导致问题的锁。其他文件指向创建某种循环等待的其他锁。
关注点
请注意,您不需要更改现有代码的任何一行。而是将两个文件添加到您的项目中。这里的技巧是让编译器使用我们的引用来实现监视器调用,而不是 .NET 的。 (在 DLL 本身中,锁被正确锁定和释放,因此您的程序在临界区方面应该可以正常工作)。这个技巧类似于在 C 中替换头文件。
请注意,该 DLL 不适用于生产环境,因为它会影响性能。另外,请注意该 DLL 允许递归锁定同一个锁。但是,即使等待时间不是无限的,该 DLL 也会通知潜在的死锁。(尽管这是一个误报,但它表明了不良行为,因为这种行为可能会影响性能,并且如果将时间设置为无限,则会导致死锁)。