在多线程编程中,开发者经常面临的关键问题之一是临界区问题。使用多个锁是常见的做法,但如果不关注锁的顺序,或者不考虑它们被调用的上下文(例如,在回调函数中),就可能形成死锁。死锁的形成有很多原因,除了明显的临界区问题外,还可能因为两个线程互相等待对方发出信号而发生,但本文不讨论这些情况。
与线程相关的任何事情,时机都是至关重要的。最棘手的死锁问题是那种很少发生的死锁,它们有一种在客户现场出现的神奇特性。如果能够将这种罕见的情况变成常态,那会怎样呢?回想一下,死锁没有发生的原因是因为可能发生死锁的两个线程并没有同时处于问题区域。因此,所要做的就是记录他们在问题区域的“访问”,然后需要验证锁的顺序始终是一致的,如果不是,输出堆栈跟踪并通知开发者发现了锁顺序的不匹配。
附加的ZIP文件包含了一个DLL,它正是这样做的。该DLL劫持了所有常见的监视器调用(包括.NET的lock关键字),并跟踪锁的顺序。一旦发现顺序问题,它就创建两个堆栈跟踪,并将指向有问题的锁的样本(修复错误后,重复测试,看看是否有另一个问题与检测到的锁在另一个流程中)。
请注意,不需要死锁真正发生;相反,重要的是怀疑的流程(或所有流程)至少执行一次。
将文件incslock.cs
添加到项目中。
添加对slockimp.dll
的引用。
编译组件并执行它。
一旦检测到问题,控制台(如果存在)将输出最后一个锁冲突与n的堆栈。在工作目录中创建了两个文件:first_xxx.txt
和now_yyy.txt
(xxx和yyy代表数字)。
转到“now”文件——找到最后一个锁(在DLL内部的最后四个调用之前)。这就是锁定时导致问题的锁。
为了找到另一个有问题的锁,可以:
这是实现的第二个版本,现在支持更复杂的场景,如哲学家就餐问题(感谢Sergey的问题)。堆栈文件按如下方式编号:0_xxx.txt
、1_xxx.txt
等...0_xxx
指向锁定时导致问题的锁。其他文件指向创建循环等待的其他锁。
请注意,不需要更改现有的任何代码行。相反,需要向项目中添加两个文件。这里的技巧是让编译器使用引用来实现监视器调用,而不是.NET的。(在DLL本身中,锁被正确地锁定和释放,所以程序在临界区方面应该可以正常工作)。这个技巧类似于在C中替换头文件。