《多核与GPU编程:工具、方法及实践》---- 3.9 调试多线程应用-阿里云开发者社区

开发者社区> 华章出版社> 正文
登录阅读全文

《多核与GPU编程:工具、方法及实践》---- 3.9 调试多线程应用

简介: 调试多线程应用不仅仅是具备一个能够管理多线程的调试器。许多现代调试器支持线程的执行和独立调试,并支持指定线程的断点、观察窗等。本节不讨论调试器的具体实现方法。例如,图3-13展示了DDD——GNU DeBugger(GDB)前端,它执行代码清单3-24中公平的读者–写者解决方案。

本节书摘来自华章出版社《多核与GPU编程:工具、方法及实践》一书中的第3章,第3.9节, 作 者 Multicore and GPU Programming: An Integrated Approach[阿联酋]杰拉西莫斯·巴拉斯(Gerassimos Barlas) 著,张云泉 贾海鹏 李士刚 袁良 等译, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。

3.9 调试多线程应用

调试多线程应用不仅仅是具备一个能够管理多线程的调试器。许多现代调试器支持线程的执行和独立调试,并支持指定线程的断点、观察窗等。本节不讨论调试器的具体实现方法。例如,图3-13展示了DDD——GNU DeBugger(GDB)前端,它执行代码清单3-24中公平的读者–写者解决方案。在UNIX/Linux中能够使用DDD和GDB(对于CLI困难)的唯一要求是在编译程序时添加调试支持选项,亦即使用编译器的-g开关。

多线程程序中的bug通常只有在与事件的某个精确时序有关的特定环境下才会出现。调试器通过暂停或者降低线程执行速度干扰了线程时序,使得bug的重现和发现是一项具有挑战性的工作。最终,发现bug是一项需要经验和直觉的任务,但是适当地植入程序代码可以帮助简化调试工作。

下面的列表中枚举了程序员为确保一个无bug的多线程应用应该遵循的步骤。

消除多线程应用中bug的第一步当然是不要在最初将其加入到程序中。在编写代码之前,一个合理的软件设计方案是十分关键的。

本章研究的经典问题不仅仅具有教学意义。实际应用中遇到的大部分并发问题,或者是这些问题的实例,或者可以通过简单的变换将其归约到这些问题上。使用本章介绍的解决方案可以避免重新设计一个新方案所带来的问题。
应用程序应该被修改为支持生成某种可以离线处理的日志或者足迹历史信息。这允许收集有关应用程序运行时行为的信息。

在应用程序中拥有过量的线程会使得生成的日志信息难于理解。就线程个数来说,将应用程序参数化一般是一个好的设计。通过限制线程数目为1,可以发现与时序无关的bug。将线程数目设置为2或者3,可以降低从日志中抽取信息的开销。


bf2bf63b7077096327ed857c1f90f09d7834879b

通过printf和cout语句来维护执行路劲以及程序状态是不够的。在同一时刻使多线程生成控制台(或者文件)输出经常会导致消息的混乱以致无法解码。解决方法是将控制台(或者文件流)作为一个共享资源对待,并且将输出语句放置到关键区中。

一种方式是将调试和追踪信息放置到内存缓冲区中(称为足迹缓冲区),并在程序终止时保存。这是一种有问题的方式,可能导致程序的错误行为。这需要程序(a)正常终止(亦即没有发生崩溃或者挂起),(b)缓冲区足够大,能存储所有生成的信息,以及(c)缓冲区没有被内存错误影响。

一个更好的解决方案是把足迹消息尽可能快地转储到控制台中。这种方式也适用于文件,但是对每一条需要保存的消息引入了打开和关闭文件的开销。否则,如果程序崩溃,可能导致文件最近更改及重要信息的丢失。

为了区分正常的程序输出和足迹消息,可以利用标准错误输出流。但是当需要进一步处理调试输出时,例如根据生成消息的线程来过滤消息等处理,又该如何设计?解决方案十分简单,即通过流重定向。这是*nix和Windows环境中的一个通用功能。因此,为了将标准错误输出重定向到trace.log文件中,可以使用下面的语句:

为所有调试消息打上其生成时间的时间戳也是一个好主意。为了实现这一目的,分辨率在1毫秒或者更低的正常时间函数(例如clock)就不能满足需求了。需要一个更高分辨率的定时器,这可以通过不同的API实现(更多细节请参考附录C.2节)。在本节后面的部分假定存在一个称为hrclock的函数(高分辨率时钟),它可以返回一个双精度类型的时间戳。

代码清单3-30 展示了一个实现前面讨论的方案的示例。


03c03125bd0576e84a71e2689990586e5839f829


a917a5baab2285fb4ffb9b9213114e87d92f8b91

这个示例程序的关键点如下。

附加的代码片段位于C++的预处理条件块中(第33~37行,以及第47~49行)。只需要将第三行注释掉就可以得到该程序的一个发布版本。

使用一个全局的互斥量来保证debugMsg函数体位于关键区中。

从程序开始执行时开始测量时间。实例的时间戳存储在time0变量中,接下来每一步都从每个计算的时间戳减去它(第15行)。

该程序的一次简单运行以及对其调试输出的仔细观察可以揭示竞争条件的工作方式。


9b5b507292e673b2438f5224a1a2963f35a3c29e

最终是一些有关恰当调试程序的警告:应该关闭编译器优化。优化编译器可能为了使得执行流更顺畅,而改变语句执行顺序,或者甚至丢弃在程序中声明的变量。因此会导致调试一个优化的程序产生奇怪的结果,例如语句间跳转。在极端情况下,可能会由于编译器的优化而导致一个bug。尽管这是异常的事件,但是还有一些编译器优化是被标识为“不安全”的。尝试将编译器发挥到极致的程序员应该确保最终应用程序的行为与未优化的行为是一致的。

例如,GCC的-ftree-loop-if-conver-stores编译选项将条件内存写操作变为一个无条件内存写操作。从编译器手册中选择的一个示例如下:


8e6ef39a98cd75de9798700d6396b2e5874fcdc4

如果数组A是一个共享资源,则两个版本的代码都可能会引入竞争条件。在后一个版本中,问题可能会被放大。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享: