使用英特尔® Inspector XE 2011 发现多线程代码中的数据竞跑 (PDF 288KB)
摘要
英特尔® Inspector XE是英特尔® Parallel Studio XE 套件产品中的三大组件之一,可用于查找或应用中的错误。英特尔 Inspector XE 可自动查找内存错误、死锁和其他可能导致死锁、数据竞跑、线程停止等的情况。
本文是“英特尔多线程应用开发指南”系列的一部分,该系列介绍了针对英特尔® 平台开发高效多线程应用的指导原则。
背景
调试线程化应用十分棘手,因为调试器会改变运行时性能,掩盖竞态条件。 甚至打印声明都可以掩盖问题,因为它们会用到同步和操作系统函数。 从可能潜伏的线程错误角度考虑以下代码示例。
我们来详细了解一些常见的线程错误。 在该代码片段中,全局变量 col 在函数 render_one_pixel 中遭到修改。 显然,如果多个线程正在写入变量 col,col 的值将以最后写入该值的线程为准。 这是数据竞跑的经典示例。
一般来说,竞态条件都很难被检测到,这是因为在给定的实例下,变量有可能会以程序函数正常运行的顺序“赢得竞争”。 而且,一项程序某一次可以正常运行并不表示它永远都能正常运行。 可以先在各种机器上测试您的程序,譬如某些支持超线程 (HT) 技术的机器和某些采用了多颗物理处理器的机器等,但由于在测试中会出现问题重复现象,因此这种方法比较繁琐,也不可预测。 英特尔 Inspector XE 等工具可提供一定的帮助。 传统调试器在探测竞态条件方面毫无用处,因为它们能让一个线程停止“竞争”,而其它线程则继续大幅度地改变运行时行为,从而对数据竞跑进行模糊化处理。
建议
使用英特尔 Inspector XE 帮助调试多线程化应用。 英特尔 Inspector XE 可提供非常有价值的并行执行信息和调试提示。 借助动态二进制插桩 (dynamic binary instrumentation),英特尔 Inspector XE 可执行应用并通用线程 API 和所有内存访问,借以识别编码错误。 它可以找到在测试中不常发生,但在客户现场时常出现的错误。 这些称之为间歇性漏洞,是多线程化编程过程中所独有的。 该工具旨在检测和查找此类错误。 请务必谨记在访问尽可能少的内存时,使用该工具测试所有代码路径,以便加快数据收集进程。 通常情况下,您需要对源代码或数据集稍作修改,以减少应用处理的数据量。
使用英特尔 Inspector XE 对程序进行分析之前,需完成一定的准备工作,例如在编译代码时禁用优化选项,启用调试符号。 通过 Windows "Start"菜单启动单机版英特尔 Inspector XE GUI。 创建新项目并指定待分析的应用和供应用运行的目录,然后点击工具栏上的"New Analysis"图标。 选择第三级: "Threading Error Analysis"下方的"Locate Deadlocks and Dataraces"。 单击"Start"启动分析。
单击"Start"后,英特尔 Inspector XE 将通过动态二进制插桩运行该应用。 应用退出后,该工具将显示查找结果汇总。
图 1. 英特尔 Inspector XE 汇总视图
图 2. 英特尔 Inspector XE 源代码视图
在英特尔 Inspector XE 的帮助下获得错误报告并确定问题根源后,开发人员便可以开始考虑采用什么方法解决问题。 下文介绍了避免并行代码中出现数据争用现象的一般考虑事项,以及关于如何修复被检查代码中所存在问题的有效建议。
修改代码以使用局部变量(而非全局变量)
在代码示例中,第 80 行的变量 col 可在第 88 行声明为局部变量。 (请见示例中提供的注释)如果线程不再引用全局数据,表明已不存在争用现象,因为每个线程都将有其自己的变量副本。 这是解决该问题的首选方法。
使用互斥控制全局数据访问
出于许多算法方面的原因需要访问全局数据,而且无法将全局变量转换为局部变量。 在这种情况下,一般使用互斥来控制对全局数据的访问,从而使线程安全地访问全局数据。
该示例正好使用英特尔® 线程构建模块(英特尔® TBB)创建并管理线程,而英特尔 Inspector XE 也可支持许多其他的线程模型。 英特尔 TBB 可提供多种互斥模式,用于控制对全局数据的访问。
在这一新的代码片段中,countMutex 声明为 scoped_lock。 scoped_lock 的语义如下:通过 scoped_lock 构造函数获取锁,并在代码离开模块之时由析构函数自动释放锁。 因此仅一个线程允许执行 render_one_pixel() 一次,如果其他线程调用 render_one_pixel(),将会命中 scoped_lock 并强制等待之前的线程完成。 使用互斥的确可以影响性能,而且必须尽可能缩小互斥范围,以使线程等待间隔达到最短。
使用并发容器控制全局数据访问
除了使用互斥控制全局数据访问外,英特尔 TBB 还可提供许多高度并发的容器类。 并发容器支持多个线程访问和更新容器内的数据。 英特尔 TBB 提供的容器可通过 2 种方法提供较高级别的并发性:精细锁定和无锁算法。 使用这些容器会产生一定的开销,因此需要折衷考虑以确定并发性加速是否能够弥补这种开销。
使用指南
英特尔 Inspector XE 目前适用于 32 位和 64 位版本的 Microsoft* Windows XP、Windows Vista 和 Windows 7 操作系统,可集成至 Microsoft* Visual Studio 2005、2008和 2010,还适用于 32 位和 64 位版本的 Linux*。
英特尔 Inspector XE 插桩提高了应用对 CPU 和内存资源的要求,因此选择规模小但极具代表性的测试问题非常重要。 运行时间只有几秒钟的工作负载将是最佳选择。 工作负载不一定是现实存在的, 只需用到多线程代码的相关代码段即可。