如何调试罕见的死锁

How to debug a rare deadlock?

本文关键字:死锁 调试 何调试      更新时间:2023-10-16

我正在尝试调试一个很少死锁的自定义线程池实现。 所以我不能使用像 gdb 这样的调试器,因为我在死锁之前点击了 100 次"启动"调试器。

目前,我正在 shell 脚本中的无限循环中运行线程池测试,但这意味着我看不到变量等。 我正在尝试std::cout数据,但这会减慢线程速度并降低死锁的风险,这意味着我可以在收到消息之前无限等待 1 小时。 然后我没有收到错误,我需要更多消息,这意味着再等一个小时......

如何有效地调试程序,使其一遍又一遍地重新启动,直到死锁? (或者也许我应该用所有代码打开另一个问题以获得帮助?

提前谢谢你!

奖励问题:如何检查std::condition_variable一切正常? 您无法真正判断哪个线程处于睡眠状态,或者wait条件上是否发生了争用条件。

有两种基本方法:

  1. 在调试器下自动运行程序。使用 gdb program -ex 'run <args>' -ex 'quit' 应在调试器下运行程序,然后退出。如果程序以一种或另一种形式仍然处于活动状态(段错误,或者您手动破坏了它),您将被要求确认。
  2. 重现死锁附加调试器。例如,gdb 可以作为gdb <program> <pid>运行以附加到正在运行的程序 - 只需等待死锁然后连接即可。当附加的调试器导致计时更改并且您无法再重现错误时,这尤其有用。

通过这种方式,您可以循环运行它并在喝咖啡时等待结果。顺便说一句 - 我发现第二种选择更容易。

如果这是某种家庭作业 - 一次又一次地重新启动并进行更多调试将是一种合理的方法。

如果有人为你等待的每个小时付费,他们可能更愿意投资一个支持基于重放的调试的软件,也就是说,一个记录程序所做的一切,每条指令的软件,并允许你一次又一次地重播它,来回调试。因此,您不会添加更多调试,而是记录发生死锁的会话,然后在死锁发生之前开始调试。你可以随心所欲地来回走动,直到你最终找到罪魁祸首。

链接中提到的软件实际上支持Linux和多线程。

Mozilla rr 开源重放 基于

调试

https://github.com/mozilla/rr

Hans 提到了基于重放的调试,但有一个特定的开源实现值得一提:Mozilla rr

首先,您执行记录运行,然后您可以根据需要多次重播完全相同的运行,并在GDB中观察它,它保留了所有内容,包括输入/输出和线程排序。

官网提到:

RR 最初的动机是使间歇性故障的调试变得容易

此外,rr使 GDB 反向调试命令(如 reverse-next)能够转到上一行,从而更容易找到问题的根本原因。

下面是rr操作的最小示例:如何转到 GDB 中的上一行?

您可以使用

https://stackoverflow.com/a/8657833/341065: gdb --eval-command=run --eval-command=quit --args ./a.out 中所示的命令在 GDB 下循环运行测试用例。

我自己用过这个:(while gdb --eval-command=run --eval-command=quit --args ./thread_testU ; do echo . ; done).

一旦它死锁并且没有退出,您只需通过 CTRL+C 中断它即可进入调试器。

查找死锁的一个简单快速调试是将一些全局变量修改到要调试的位置,然后将其打印在信号处理程序中。您可以使用SIGINT(在中断时发送ctrl+c)或SIGTERM(在杀死程序时发送):

int dbg;
int multithreaded_function()
{
  signal(SIGINT, dbg_sighandler);
  ...
  dbg = someVar;
  ...  
}
void  dbg_sighandler(int)
{
  std::cout << dbg1 << std::endl;
  std::exit(EXIT_FAILURE);
}

这样,当您使用 ctrl+c 中断程序时,您只会看到所有调试变量的状态。

此外,您可以在 shell 中运行它 while 循环:

$> while [ $? -eq 0 ]
   do
   ./my_program
   done

它将永远运行您的程序,直到它失败($?是程序的退出状态,您在信号处理程序中以EXIT_FAILURE退出)。

它对我来说效果很好,特别是对于找出在锁定之前和之后传递了多少线程。

它非常质朴,但您不需要任何额外的工具,并且可以快速实现。