如何调试C++ Qt程序中的多线程死锁?

Nya*_*uko 0 c++ qt multithreading

对于多线程死锁错误(或其他与多线程相关的错误),这种错误很少发生且难以重复,并且当发生这种情况时,程序会在 Windows 下冻结,因此我什至无法将调试器附加到它。有时,该错误甚至仅在某些特定条件下发生(例如当 CPU 繁忙时)。对于这些错误,有什么神奇的软件或技术可以拯救我的日子吗?

编辑:抱歉我的问题不太具体,我只是在调试一个巨大的软件,没有其他人编写的任何一行文档。所以我只是想知道是否有类似代码分析器或任何有用的技术可以快速检测多线程错误?

Jer*_*ner 5

如果您可以在 MacOS/X 下运行您的程序,那么当您的程序处于死锁状态时,要做的一件有用的事情就是打开“活动监视器”,选择您的进程,然后执行“示例进程”或“运行 Spindump”。这将向您显示进程中每个线程的当前堆栈跟踪 - 您很可能会发现两个(或更多)线程在 lock() 调用中被阻塞,并且通过检查这些 lock() 调用的位置,您可以计算出找出哪些互斥体导致了死锁。这对于弄清楚僵局是如何发生的有很大帮助。

或者,如果您可以在 Linux 下构建程序,则可以使用 valgrind 的helgrind工具来检测潜在的死锁。我不确定其他操作系统可能有类似的工具。

如果没有,那么您需要分析程序中获取锁的所有序列。对于小程序,您可以通过眼睛来完成此操作 - 遍历所有代码路径并记下获取的锁以及获取的顺序。您特别感兴趣的是一个线程一次持有多个锁的情况 - 如果有另一个线程也持有这些锁,并且另一个线程没有按照与第一个线程相同的顺序获取这些锁线程,这是一个潜在的死锁。另一方面,线程获取单个锁然后释放它(同时不获取任何其他锁)的情况永远不会导致死锁,因此您可以忽略它们。

如果您的程序太大/太复杂,无法进行手动分析,并且您没有自动工具来检查锁获取序列,那么您可以通过一些工作“自行开发”。您想要做的是使用包装函数搜索并替换程序的所有锁定命令,该包装函数在获取锁定之前打印出一些调试信息。包装函数可能如下所示:

#define my_lock(theMutex) my_lock_aux(__FILE__, __LINE__, theMutex)
void my_lock_aux(const char * file, int line, QMutex & theMutex)
{
   printf("Thread %i is about to lock mutex %p at [%s:%i]\n", (int)pthread_self(), &theMutex, file, line);
   theMutex.lock();
}
Run Code Online (Sandbox Code Playgroud)

...并对您的unlock() 调用执行类似的操作:

#define my_unlock(theMutex) my_unlock_aux(__FILE__, __LINE__, theMutex)
void my_unlock_aux(const char * file, int line, QMutex & theMutex)
{
  theMutex.unlock();
  printf("Thread %i unlocked mutex %p at [%s:%i]\n", (int)pthread_self(),  &theMutex, file, line);
}
Run Code Online (Sandbox Code Playgroud)

一旦完成并编译,您就可以运行您的程序,它会将大量输出打印到标准输出。将其重定向到一个文件,给您的程序一些练习(注意:您实际上不必重现死锁,您只需要获取程序行为的代表性示例),然后退出程序。

现在您有一个文本文件,其中包含“线程 1234 即将在 [...] 处锁定互斥体 blah”和“线程 31415 在 [...] 处解锁互斥体 blah”消息,您可以编写一个小程序来解析该消息文件来自动确定每个线程同时持有的互斥体集,以及该线程获取这些互斥体的顺序。

一旦该程序完成对文件的解析,您就可以让它打印出它找到的所有多重锁定获取序列,以便您可以直观地看到不一致的顺序在哪里;例如,它可能会打印出如下内容:

 Thread 1234 acquired 4 locks simultaneously:
    -> Lock 0x1236782 was acquired at somefile.cpp:128
    -> Lock 0x2304890 was acquired at anotherfile.cpp:57
    -> Lock 0x0945820 was acquired at yetanotherfile.c:562
    -> Lock 0x2345824 was acquired at somefile.c:125

 Thread 4261 acquired 2 locks simultaneously:
    -> Lock 0x0945820 was acquired at yetanotherfile.c:562
    -> Lock 0x2304890 was acquired at anotherfile.cpp:57
Run Code Online (Sandbox Code Playgroud)

...然后您会注意到线程 4261 获取锁的顺序与线程 1234 不同,从而引入了可能的死锁。然后,您需要弄清楚如何修改程序,以便两个线程以相同的顺序获取这些互斥体...或者更好的是,修改它,以便线程不必同时锁定多个互斥体根本没有时间。

如果您想更进一步,您甚至可以编写一个函数来比较所有序列,并标记那些顺序代表潜在死锁的序列。如果存在大量序列,这可能比尝试通过肉眼检测它们更可靠。

FWIW,是我编写的用于进行上述日志文件解析和分析的程序的源代码;也许它作为一个例子会有帮助(或者也许它只会让你感到困惑,在这种情况下请忽略它)。