如何检测和调试多线程问题?

这是对 这个问题的跟进,我在这一点上没有得到任何输入。以下是一个简短的问题:

是否有可能检测和调试来自多线程代码的问题?

我们经常不得不告诉我们的客户: “我们不能在这里重现问题,所以我们不能解决它。请告诉我们重现问题的步骤,然后我们会解决它。”如果我知道这是一个多线程问题,那么这是一个令人讨厌的答案,但大多数情况下我并不知道。我如何知道一个问题是一个多线程问题,以及如何调试它?

我想知道是否有任何特殊的日志框架、调试技术、代码检查员或其他帮助解决这些问题的方法。欢迎一般性接触。如果有任何答案应该与语言有关,然后保持它。NET 和 Java。

64089 次浏览

我认为你得到的 回答到你的 另一个问题是相当好的。但我会强调这些点。

只修改关键部分的共享状态(互斥锁)

按照设定的顺序获取锁,然后按照相反的顺序释放它们。

只要有可能就使用预先构建的抽象 (就像 java.util.current 中的内容一样)

此外,一些分析工具可以检测到一些潜在的问题。例如,FindBugs可以在 Java 程序中发现一些线程问题。这样的工具不能发现所有的问题(它们不是灵丹妙药) ,但它们可以提供帮助。

正如 Vanslly在对这个答案的评论中指出的那样,研究适当位置的日志输出也非常有帮助,但是要小心 海森堡

假设我有很难重现的故障报告,我总是通过阅读代码发现这些故障,最好是双代码阅读,这样您就可以讨论线程语义/锁需求。当我们基于 报告的问题进行这项工作时,我发现我们总是能够相当快地解决一个或多个问题。我认为这也是一个相当廉价的技术来解决难题。

对不起,我不能告诉你按 ctrl + shift + f13,但我不认为有这样的东西可用。但是考虑到 什么报告的问题实际上 通常在代码中给出一个相当强的方向感,所以您不必从 main ()开始。

VisualStudio 允许您检查每个线程的调用堆栈,并且可以在它们之间切换。虽然跟踪各种线程问题远远不够,但这是一个开始。在即将到来的 VS2010中,计划对多线程调试进行大量改进。

我使用 WinDbg + SoS 来解决。NET 代码。您可以检查锁(同步块) ,线程调用堆栈等。

众所周知,线程/并发问题 很难复制——这就是为什么应该设计避免或至少尽量减少这种可能性的原因之一。这就是不可变对象如此有价值的原因。尝试将可变对象隔离到单个线程,然后仔细控制线程之间可变对象的交换。尝试用对象移交的设计来编程,而不是“共享”对象。对于后者,使用完全同步的控制对象(这更容易理解) ,避免让同步对象利用其他也必须同步的对象——也就是说,尽量保持它们自包含。你最好的防御是一个好的设计。

如果死锁时可以获得堆栈跟踪,那么死锁 是最容易调试的。给定跟踪(其中大部分都进行死锁检测) ,很容易查明原因,然后推断代码为什么要修复它以及如何修复它。对于死锁,以不同的顺序获取相同的锁总是一个问题。

活动锁 比较困难——当系统处于错误状态时,能够观察系统是最好的选择。

竞争条件 往往极难复制,甚至更难从手动代码检查中识别出来。有了这些,我通常采取的路径,除了广泛的测试复制,是推理的可能性,并尝试记录信息,以证明或反驳理论。如果你有国家腐败的直接证据,你可以根据腐败的可能原因进行推理。

系统越复杂,就越难发现并发错误,也就越难对其行为进行推理。使用像 JVisualVM 和远程连接分析器这样的工具——如果你能够连接到一个处于错误状态的系统并检查线程和对象,它们可以成为你的救命稻草。

另外,注意可能的行为差异,这取决于 CPU 核心、管道、总线带宽等的数量。硬件的更改会影响您复制问题的能力。有些问题只会显示在单核 CPU 的其他只有在多核。

最后一件事,尝试使用分布在系统库中的并发对象-例如,Javajava.util.concurrent是您的朋友。写自己的并发控制很难,而且充满危险,如果你有选择的话,把它留给专家吧。

除了您已经得到的其他好答案之外: 始终在具有至少与客户所使用的处理器/处理器核心一样多的机器上进行测试,或者根据程序中有活动线程的情况进行测试。否则,一些多线程程序错误可能难以复制。

开发代码 就像公主为你的另一个问题推荐的那样(不可变对象和 Erlang 样式的消息传递)。检测多线程问题将更加容易,因为线程之间的交互将得到很好的定义。

除了崩溃转储之外,还有一种技术是广泛的运行时日志记录: 每个线程在其中记录它正在做的事情。

当报告错误时,第一个问题可能是“日志文件在哪里?”

有时您可以在日志文件中看到这个问题: “这个线程在这里检测到一个非法/意外的状态... ... 看,另一个线程正在这样做,就在这之前和/或之后。”

如果日志文件没有说明发生了什么,那么就向客户道歉,充分地向代码中添加——许多额外的日志语句,将新代码交给客户,并说在它再次发生之后您将修复它。

Tess Ferrandez 的 blog 有很多使用 WinDbg 在.NET 中调试死锁的好例子。

我能想到的最好的办法就是尽可能远离多线程代码。似乎很少有程序员能够编写没有 bug 的多线程应用程序,我认为没有程序员能够编写没有 bug 的 很大多线程应用程序。

断言()是检测竞态条件的朋友。无论何时输入临界区,都要断言与其相关的不变量为 true (这就是 CS 的用途)。虽然,不幸的是,检查可能是昂贵的,因此不适合在生产环境中使用。

对于 Java 来说,有一个名为 Javapathfinder的验证工具,我发现它对于针对潜在的竞态条件和代码中的死锁 bug 来调试和验证多线程应用程序是很有用的。
它可以很好地与 Eclipse 和 Netbean IDE 一起工作。

[2019] github 知识库 Https://github.com/javapathfinder

我遇到了一个线程问题,它给出了相同的错误结果,并且没有不可预测的行为,因为每次其他条件(内存、调度器、处理负载)或多或少都是相同的。

根据我的经验,我可以说最难的部分是认识到它是一个线程问题,而最好的解决方案是仔细检查多线程代码。只要仔细查看线程代码,就可以尝试找出可能出错的地方。其他方法(线程转储、分析器等)将紧随其后。

我实现了工具 Vmlen来检测运行时 Java 程序中的竞态条件,它实现了一个名为 橡皮擦的算法。

有时,无法避免多线程解决方案。如果存在 bug,则需要实时地对其进行调查,而对于 VisualStudio 等大多数工具来说,这几乎是不可能的。唯一可行的解决方案是编写跟踪,尽管跟踪本身应该:

  1. 不要增加任何延迟
  2. 不要使用任何锁定
  3. 多线程安全
  4. 按照正确的顺序追踪发生的事情。

这听起来像是一个不可能完成的任务,但是可以通过将跟踪写入内存来轻松实现。在 C # 中,它看起来像这样:

public const int MaxMessages = 0x100;
string[] messages = new string[MaxMessages];
int messagesIndex = -1;


public void Trace(string message) {
int thisIndex = Interlocked.Increment(ref messagesIndex);
messages[thisIndex] = message;
}

方法 Trace ()是多线程安全的,非阻塞的,可以从任何线程调用。在我的电脑上,执行大约需要2微秒,这应该足够快了。

在您认为可能出现错误的地方添加 Trace ()指令,让程序运行,等待错误发生,停止跟踪,然后调查跟踪是否有错误。

这种方法的更详细的描述还收集线程和计时信息,回收缓冲区并输出跟踪,您可以在以下位置找到: CodeProject: 实时调试多线程代码

在调试多线程代码时要记住的一些调试技术的一个小图表。 该图表正在增长,请留下评论和提示添加。 (在 这个链接更新档案)

Multithreaded debugging chart

我使用 GNU 和简单的脚本

$more gdb _ tracer

b func.cpp:2871
r
#c
while (1)
next
#step
end

缩小正在调用的函数的范围,并排除哪些函数应该受到指责,哪些函数不应该受到指责。当您发现可能导致问题的代码段时,请向其添加大量详细的日志记录/跟踪。一旦问题再次发生,检查日志以查看代码执行情况与“基线”情况下的执行情况有何不同。

如果使用 VisualStudio,还可以设置断点并使用“并行堆栈”窗口。在调试并发代码时,并行堆栈是一个巨大的帮助,它将使您能够在线程之间切换以独立调试线程。更多信息

Https://learn.microsoft.com/en-us/visualstudio/debugger/using-the-parallel-stacks-window?view=vs-2019

Https://learn.microsoft.com/en-us/visualstudio/debugger/walkthrough-debugging-a-parallel-application?view=vs-2019