最近,我一直在从事大量使用线程的项目。我认为我可以设计它们。尽可能使用无状态设计,锁定对一个线程以上需求的所有资源的访问,等等。我在函数式编程中的经验对此提供了极大的帮助。
但是,在阅读别人的线程代码时,我会感到困惑。我现在正在调试死锁,并且由于编码样式和设计与我的个人风格不同,因此我很难看到潜在的死锁情况。
调试死锁时您会寻找什么?
最近,我一直在从事大量使用线程的项目。我认为我可以设计它们。尽可能使用无状态设计,锁定对一个线程以上需求的所有资源的访问,等等。我在函数式编程中的经验对此提供了极大的帮助。
但是,在阅读别人的线程代码时,我会感到困惑。我现在正在调试死锁,并且由于编码样式和设计与我的个人风格不同,因此我很难看到潜在的死锁情况。
调试死锁时您会寻找什么?
Answers:
如果情况是真正的死锁(即,两个线程持有两个不同的锁,但至少一个线程想要一个锁,另一个线程持有),则需要首先放弃所有关于线程如何排序锁定的先入之见。假设什么都不做。您可能希望从正在查看的代码中删除所有注释,因为这些注释可能会使您相信某些不正确的内容。很难强调这一点:不承担任何责任。
之后,确定在线程尝试锁定其他对象时持有哪些锁定。如果可以,请确保线程以相反的顺序从锁定中解锁。更好的是,确保线程一次仅持有一个锁。
认真研究线程的执行,并检查所有锁定事件。在每个锁处,确定一个线程是否持有其他锁,如果是,则在什么情况下执行类似执行路径的另一个线程可以进入所考虑的锁事件。
当然,在时间或金钱用完之前,您可能不会发现问题。
正如其他人所说的...如果您可以获取有用的日志信息,请先尝试一下,因为这是最容易的事情。
确定所涉及的锁。将所有永久等待的互斥锁/信号量更改为定时等待...有点长,就像5分钟一样。超时时记录错误。这至少将使您指向该问题所涉及的锁之一的方向。根据时间的可变性,您可能会很幸运,在几次运行后都找到了两个锁。在定时等待未能确定您最初到达那里的方式之后,请使用函数失败代码/条件记录伪堆栈跟踪。这应该可以帮助您确定问题所涉及的线程。
您可以尝试的另一件事是围绕互斥/信号量服务构建包装器库。跟踪每个互斥量有哪些线程,以及在互斥量上等待什么线程。构建一个监视线程,以检查线程被阻塞了多长时间。在合理的持续时间内触发并转储您正在跟踪的状态信息。
在某个时候,将有必要进行简单的旧代码检查。
第一步(如Péter所说)是日志记录。虽然根据我的经验,这通常是有问题的。在繁重的并行处理中,这通常是不可能的。我必须使用神经网络调试一次类似的东西,每秒处理10万个节点。该错误仅在几个小时后才发生,甚至单行输出都使速度变慢,以至于需要几天的时间。如果可以进行日志记录,则应将精力集中在程序流程上,而不是集中在数据上,直到知道发生在哪一部分。在每个函数的开头仅需一行,如果可以找到合适的函数,请将其拆分为较小的块。
另一个选择是删除部分代码和数据以本地化该错误。甚至可能编写了一个仅包含某些类并且仅运行最基本测试的小程序(当然仍然在多个线程中运行)。删除与gui相关的所有内容,例如有关实际处理状态的所有输出。(我发现用户界面经常是该错误的来源)
在您的代码中,尝试遵循初始化锁和释放锁之间的完整控制逻辑流程。一个常见的错误可能是在函数开始时锁定,在函数结束时解锁,但在两者之间有条件返回语句。异常也会阻止发布。
我最好的朋友是代码中有趣位置的打印/日志语句。这些通常可以帮助我更好地了解应用程序内部的实际运行情况,而又不会中断不同线程之间的时间安排,而这可能会阻止重新生成该错误。
如果失败了,我唯一剩下的方法就是盯着代码,尝试建立各种线程和交互的思维模型,并尝试思考可能的疯狂方法以实现显然发生的事情:-)但是我没有认为自己是一个非常有经验的死锁杀手。希望其他人能够提出更好的想法,我也可以从中学习:-)
首先,尝试获得该代码的作者。他可能会知道他写了什么。即使你们两个不能仅仅通过讲话来查明问题,至少您也可以和他一起坐下来查明死锁部分,这比您在没有帮助的情况下理解他/她的代码要快得多。
像PéterTörök所说的那样,日志记录可能是失败的方法。据我所知,Debugger在多线程环境中做得不好。尝试找到锁的位置,获取正在等待的资源,以及在什么情况下发生赛车状况。
这个问题吸引了我;)首先,请自以为幸运,因为您能够在每次运行中始终如一地重现该问题。如果您每次都收到具有相同堆栈跟踪的相同异常,那么它应该很简单。如果不是,那么就不要那么信任stacktrace,而只是监视对全局对象的访问及其在执行过程中状态的变化。
如果您必须调试死锁,那么您已经遇到麻烦了。通常,请使用锁以最短的时间-或尽可能不使用锁。任何情况下都应避免使用锁然后转到非平凡的代码。
当然,这取决于您的编程环境,但是您应该查看顺序队列之类的事物,这些事物可能仅允许您从单个线程访问资源。
然后是一个古老但可靠的策略:为每个锁分配一个“级别”,从级别0开始。如果您使用级别0的锁,则不允许使用其他任何锁。取得1级锁后,您可以取得0级锁。取得10级锁后,您可以取得9级或更低级别的锁,等等。
如果发现这不可能完成,则需要修复代码,因为这将导致死锁。