C编程:使用pthreads进行调试

我最初调整到的最困难的事情之一就是我第一次使用C语言编写C语言的强烈经验。我习惯于确切知道下一行代码的运行方式是什么,而且我的大多数调试技术都围绕着这种期望。

在C中用pthreads调试有哪些好的技巧? 您可以建议个人方法,无需任何添加工具,使用的工具或任何其他有助于您调试的方法。

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

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

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

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

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

在一个线程程序中,Heisenbug通常意味着你有一个竞争条件 。 一个优秀的程序员将寻找依赖于顺序的共享变量或资源。 一个蹩脚的程序员会试着用sleep()语句盲目地修复它。

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

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

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

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

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

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

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

  3. 如有必要,添加可能反驳或validation结果和理论的代码。

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

通常,certificate该理论的“实验”是围绕可疑代码添加关键部分或互斥体。 然后,我将尝试通过系统地缩小关键部分来缩小问题范围。 关键部分并不总是最好的解决方案(尽管通常可以快速修复)。 但是,它们对于精确定位“吸烟枪”非常有用。

就像我说的那样,相同的步骤适用于单线程调试,尽管它很容易跳入调试器并且很容易。 multithreading调试需要对代码有更强的理解,因为我通常发现通过调试器运行的multithreading代码不会产生任何有用的东西。

而且,hellgrind是一个很棒的工具。 英特尔的线程检查程序为Windows执行类似的function,但成本远高于他的成本。

我几乎在一个独特的multithreading,高性能世界中发展,所以这是我使用的一般做法。

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

1)将function分解为LOGICALLY可分离的部分。 这意味着一个呼叫做“A”而只有“A” – 不是A然后B然后C ……
2)无副作用:取消所有裸露的全局变量,静态或不静态。 如果您无法完全消除副作用,请将它们隔离到几个位置(将它们集中在代码中)。
3)尽可能多地制作隔离组件RE-ENTRANT。 这意味着它们是无状态的 – 它们将所有输入作为常量,并且只操作DECLARED,逻辑上恒定的参数来产生输出。 无论您在哪里,都可以通过值传递而不是参考。
4)如果你有状态,在无状态子组件和实际状态机之间做一个明确的分离。 理想情况下,状态机将是一个操作无状态组件的单个函数或类。

调试:

线程错误往往有两种广泛的种族和僵局。 通常,死锁更具确定性。

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

断点通常与代码中的同步原语THEMSELVES非常相似,因为它们在逻辑上相似 – 它们强制执行在当前上下文中停止,直到某些其他上下文(您)发送信号以恢复。 这意味着您应该在代码中查看任何断点以改变其multithreading行为,并且断点将影响竞争条件,但(通常)不会出现死锁。

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

我倾向于使用大量断点。 如果你实际上并不关心线程函数,但确实关心它的副作用,那么检查它们的好时机可能会在它退出或循环回到它的等待状态或其他任何其他状态之前。

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

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

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