调试死锁时您会寻找什么?


25

最近,我一直在从事大量使用线程的项目。我认为我可以设计它们。尽可能使用无状态设计,锁定对一个线程以上需求的所有资源的访问,等等。我在函数式编程中的经验对此提供了极大的帮助。

但是,在阅读别人的线程代码时,我会感到困惑。我现在正在调试死锁,并且由于编码样式和设计与我的个人风格不同,因此我很难看到潜在的死锁情况。

调试死锁时您会寻找什么?


我在这里而不是在这里问这个问题,是因为我想要更多有关调试死锁的通用指针,而不是我的问题的具体答案。
Michael K

我能想到的策略是日志记录(正如其他一些人指出的那样),实际上是检查谁在等待谁持有锁的死锁图(有关某些信息,请参见stackoverflow.com/questions/3483094/…)指针)和锁定注释(请参见clang.llvm.org/docs/ThreadSafetyAnalysis.html)。即使不是您的代码,您也可以尝试说服作者添加注释-他们可能会在此过程中发现错误并加以修复(可能包括您的错误)。
唐·哈奇

Answers:


23

如果情况是真正的死锁(即,两个线程持有两个不同的锁,但至少一个线程想要一个锁,另一个线程持有),则需要首先放弃所有关于线程如何排序锁定的先入之见。假设什么都不做。您可能希望从正在查看的代码中删除所有注释,因为这些注释可能会使您相信某些不正确的内容。很难强调这一点:不承担任何责任。

之后,确定在线程尝试锁定其他对象时持有哪些锁定。如果可以,请确保线程以相反的顺序从锁定中解锁。更好的是,确保线程一次仅持有一个锁。

认真研究线程的执行,并检查所有锁定事件。在每个锁处,确定一个线程是否持有其他锁,如果是,则在什么情况下执行类似执行路径的另一个线程可以进入所考虑的锁事件。

当然,在时间或金钱用完之前,您可能不会发现问题。


4
+1哇,那太悲观了,虽然不是事实。鉴于您找不到所有错误,这是有根据的。感谢您的建议!
迈克尔·K

布鲁斯,您的“真正的僵局”特征令我惊讶。我认为两个线程之间的僵局是当每个线程都在等待另一个持有的锁时。您的定义似乎还包括以下情况:一个线程在持有一个锁的同时等待获取当前由另一个线程持有的第二个锁。对我来说,这听起来不像是僵局。是吗??
唐·哈奇

@DonHatch-我说得不好。您描述的情况并非僵局。我本来希望传达调试状态的混乱情况,该情况包括持有锁A的线程,然后尝试获取锁B,而持有锁B的线程则试图获取锁A。也许。也许情况要复杂得多。您只需要对锁获取的顺序保持开放的态度。检查所有假设。不要相信。
Bruce Ediger

+1建议仔细阅读代码并单独检查所有锁定操作。通过仔细检查单个节点来查看复杂的图要比立即查看整个节点要容易得多。仅仅盯着代码并在脑海中运行不同的场景,我发现了多少次问题。
Newtopian's

11
  1. 正如其他人所说的...如果您可以获取有用的日志信息,请先尝试一下,因为这是最容易的事情。

  2. 确定所涉及的锁。将所有永久等待的互斥锁/信号量更改为定时等待...有点长,就像5分钟一样。超时时记录错误。这至少将使您指向该问题所涉及的锁之一的方向。根据时间的可变性,您可能会很幸运,在几次运行后都找到了两个锁。在定时等待未能确定您最初到达那里的方式之后,请使用函数失败代码/条件记录伪堆栈跟踪。这应该可以帮助您确定问题所涉及的线程。

  3. 您可以尝试的另一件事是围绕互斥/信号量服务构建包装器库。跟踪每个互斥量有哪些线程,以及在互斥量上等待什么线程。构建一个监视线程,以检查线程被阻塞了多长时间。在合理的持续时间内触发并转储您正在跟踪的状态信息。

在某个时候,将有必要进行简单的旧代码检查。


6

第一步(如Péter所说)是日志记录。虽然根据我的经验,这通常是有问题的。在繁重的并行处理中,这通常是不可能的。我必须使用神经网络调试一次类似的东西,每秒处理10万个节点。该错误仅在几个小时后才发生,甚至单行输出都使速度变慢,以至于需要几天的时间。如果可以进行日志记录,则应将精力集中在程序流程上,而不是集中在数据上,直到知道发生在哪一部分。在每个函数的开头仅需一行,如果可以找到合适的函数,请将其拆分为较小的块。

另一个选择是删除部分代码和数据以本地化该错误。甚至可能编写了一个仅包含某些类并且仅运行最基本测试的小程序(当然仍然在多个线程中运行)。删除与gui相关的所有内容,例如有关实际处理状态的所有输出。(我发现用户界面经常是该错误的来源)

在您的代码中,尝试遵循初始化锁和释放锁之间的完整控制逻辑流程。一个常见的错误可能是在函数开始时锁定,在函数结束时解锁,但在两者之间有条件返回语句。异常也会阻止发布。


“异常可能会阻止发布”->怜悯没有范围变量的语言:/
Matthieu M.

1
@Matthieu:拥有范围变量并正确使用它们可能是两件事。而且,他在不提及特定语言的情况下总体上询问了可能的问题。因此这是一件事,可能会影响控制流程。
thorstenmüller2011年

3

我最好的朋友是代码中有趣位置的打印/日志语句。这些通常可以帮助我更好地了解应用程序内部的实际运行情况,而又不会中断不同线程之间的时间安排,而这可能会阻止重新生成该错误。

如果失败了,我唯一剩下的方法就是盯着代码,尝试建立各种线程和交互的思维模型,并尝试思考可能的疯狂方法以实现显然发生的事情:-)但是我没有认为自己是一个非常有经验的死锁杀手。希望其他人能够提出更好的想法,我也可以从中学习:-)


1
今天,我调试了几个死锁。诀窍是用一个宏包装pthread_mutex_lock(),该宏在获取锁之前和之后打印函数,行号,文件名和互斥变量的名称(通过标记化)。对pthread_mutex_unlock()也执行相同的操作。当我看到线程被冻结时,我只需要查看最后两个消息,就有两个线程试图锁定但从未完成!现在剩下的就是添加一种在运行时进行切换的机制。:-)
Plumenator

3

首先,尝试获得该代码的作者。他可能会知道他写了什么。即使你们两个不能仅仅通过讲话来查明问题,至少您也可以和他一起坐下来查明死锁部分,这比您在没有帮助的情况下理解他/她的代码要快得多。

像PéterTörök所说的那样,日志记录可能是失败的方法。据我所知,Debugger在多线程环境中做得不好。尝试找到锁的位置,获取正在等待的资源,以及在什么情况下发生赛车状况。


不,在这里记录日志是您的敌人-当您缓慢登录时,您会将程序的行为更改为易于获得在启用了日志记录的情况下可以正常运行的程序的程度,但是在关闭日志记录时会死锁。与在单核而不是多核CPU上运行程序时遇到的问题相同。
gbjbaanb

@gbjbaanb,我认为这是你的敌人太残酷了。说它是你最好的朋友,偶尔会让你失望的也许是正确的。我会在此页面上与其他几个人保持一致,他们说在检查代码失败后,日志记录是迈出的第一步,通常(实际上,根据我的经验,在大多数情况下),会找到一种简单的日志记录策略问题很容易就解决了。否则,一定要求助于其他方法,但是我认为避免尝试最适合该工作的最佳工具并不是一个好建议,因为它并不总是有用。
唐·哈奇

0

这个问题吸引了我;)首先,请自以为幸运,因为您能够在每次运行中始终如一地重现该问题。如果您每次都收到具有相同堆栈跟踪的相同异常,那么它应该很简单。如果不是,那么就不要那么信任stacktrace,而只是监视对全局对象的访问及其在执行过程中状态的变化。


0

如果您必须调试死锁,那么您已经遇到麻烦了。通常,请使用锁以最短的时间-或尽可能不使用锁。任何情况下都应避免使用锁然后转到非平凡的代码。

当然,这取决于您的编程环境,但是您应该查看顺序队列之类的事物,这些事物可能仅允许您从单个线程访问资源。

然后是一个古老但可靠的策略:为每个锁分配一个“级别”,从级别0开始。如果您使用级别0的锁,则不允许使用其他任何锁。取得1级锁后,您可以取得0级锁。取得10级锁后,您可以取得9级或更低级别的锁,等等。

如果发现这不可能完成,则需要修复代码,因为这将导致死锁。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.