31

对我来说,最难适应的事情之一是我第一次在 C 中使用 pthreads 编程的丰富经验。我习惯于确切地知道要运行的下一行代码是什么,并且我的大多数调试技术都围绕着这个期望。

在 C 中使用 pthread 进行调试有哪些好的技术?您可以在没有任何附加工具、您使用的工具或任何其他帮助您调试的情况下建议个人方法。

PS 我在 linux 中使用 gcc 进行 C 编程,但不要让这必然限制你的答案

4

8 回答 8

32

Valgrind是查找竞态条件和 pthreads API 滥用的绝佳工具。它保留程序内存(可能还有共享资源)访问的模型,并且即使错误是良性的也会检测丢失的锁(这当然意味着它会在以后的某个时候完全出乎意料地变得不那么良性)。

要使用它,你调用valgrind --tool=helgrind这里是它的手册。此外,还有valgrind --tool=drd手册)。Helgrind 和 DRD 使用不同的模型,因此它们可以检测到重叠但可能不同的错误集。也可能出现误报。

无论如何,valgrind 为我节省了无数小时的调试时间(虽然不是全部:)。

于 2009-06-11T16:05:54.510 回答
7

调试线程程序会让您感到惊讶的一件事是,您经常会发现错误更改,甚至在您添加 printf 或在调试器中运行程序时消失(俗称Heisenbug)。

在线程程序中,Heisenbug 通常意味着您有竞争条件。一个好的程序员会寻找与顺序相关的共享变量或资源。蹩脚的程序员会尝试用 sleep() 语句盲目地修复它。

于 2009-06-11T13:44:12.570 回答
2

调试多线程应用程序很困难。一个好的调试器,例如用于 *nix 环境的GDB(带有可选的DDD前端)或 Windows 上的 Visual Studio 附带的调试器将有很大帮助。

于 2009-06-11T13:28:12.720 回答
2

在“思考”阶段,在开始编码之前,请使用状态机概念。它可以使设计更加清晰。

printf 可以帮助您了解程序的动态。但是它们使源代码混乱,因此使用宏 DEBUG_OUT() 并在其定义中使用布尔标志启用它。更好的是,使用您通过“kill -USR1”发送的信号设置/清除此标志。将输出发送到带有时间戳的日志文件。

还可以考虑使用 assert(),然后使用 gdb 和 ddd 分析您的核心转储。

于 2010-02-13T15:17:25.997 回答
1

我的多线程调试方法和单线程类似,但更多的时间通常花在思考阶段:

  1. 制定一个关于什么可能导致问题的理论。

  2. 确定如果理论是正确的,可以预期什么样的结果。

  3. 如有必要,添加可以反驳或验证您的结果和理论的代码。

  4. 如果您的理论是正确的,请解决问题。

通常,证明该理论的“实验”是在可疑代码周围添加关键部分或互斥锁。然后,我将尝试通过系统地缩小临界区来缩小问题的范围。关键部分并不总是最好的解决方案(尽管通常可以是快速解决方案)。但是,它们对于查明“确凿证据”很有用。

就像我说的那样,同样的步骤也适用于单线程调试,尽管只是跳到调试器中并使用它太容易了。多线程调试需要对代码有更深入的理解,因为我通常发现通过调试器运行的多线程代码不会产生任何有用的东西。

此外,hellgrind 是一个很棒的工具。Intel 的 Thread Checker 为 Windows 执行类似的功能,但成本比 hellgrind 高很多。

于 2009-06-12T01:22:23.063 回答
1

我几乎是在一个专门的多线程、高性能世界中开发的,所以这是我使用的一般做法。

设计——最好的优化是更好的算法:

1)将您的功能分解为逻辑上可分离的部分。这意味着调用执行“A”并且仅执行“A”-不是 A,然后是 B,然后是 C...
2) 没有副作用:废除所有裸露的全局变量,无论是否静态。如果您不能完全消除副作用,请将它们隔离到几个位置(将它们集中在代码中)。
3) 使尽可能多的孤立组件 RE-ENTRANT。这意味着它们是无状态的——它们将所有输入作为常量,并且只操作 DECLARED、逻辑上的常量参数来产生输出。尽可能按值传递而不是引用。
4)如果你有状态,明确区分无状态子程序集和实际状态机。理想情况下,状态机将是操作无状态组件的单个函数或类。

调试:

线程错误往往有两种广泛的形式——竞争和死锁。通常,死锁更具确定性。

1) 你看到数据损坏了吗?:是 => 可能是一场比赛。
2) 错误出现在每次运行还是仅在某些运行时出现?:是 => 可能是死锁(比赛通常是非确定性的)。
3)进程是否挂起?:是=>某处出现死锁。如果它只是偶尔挂起,你可能也有一场比赛。

断点在代码中的作用通常很像同步原语 THEMSELVES,因为它们在逻辑上是相似的——它们强制执行在当前上下文中停止,直到其他上下文(您)发送信号恢复。这意味着您应该将代码中的任何断点视为改变其多线程行为,并且断点将影响竞争条件,但(通常)不会影响死锁。

通常,这意味着您应该删除所有断点,确定错误类型,然后重新引入它们以尝试修复它。否则,他们只会更加扭曲事物。

于 2014-07-17T17:15:20.347 回答
0

我倾向于使用很多断点。如果您实际上并不关心线程函数,但确实关心它的副作用,那么最好在它退出或循环回到它的等待状态或它正在做的任何其他事情之前检查它们。

于 2009-06-11T13:34:34.623 回答
0

当我开始进行多线程编程时,我...停止使用调试器。对我来说,关键是良好的程序分解和封装。

监视器是无错误多线程编程的最简单方法。如果您无法避免复杂的锁依赖关系,那么很容易检查它们是否是循环的 - 等到程序挂起并使用“pstack”检查堆栈跟踪。您可以通过引入一些新线程和异步通信缓冲区来打破循环锁。

使用断言,并确保为软件的特定组件编写单线程单元测试——如果需要,您可以在调试器中运行它们。

于 2012-09-19T20:28:08.957 回答