众所周知并行程序设计易于产生 bug。更为严重的是,往往在开发过程的晚期当这些并行 bug 引起严重的损害时才能发现它们并且难于调试它们。即使彻底地对它们进行了调试,常规的单元测试实践也很可能遗漏并行 bug。在本文中,并行专家 Shmuel Ur 和 Yarden Nir-Buchbinder 解释了为什么并行 bug 如此难于捕获并且介绍了 IBM Research 的一种新的解决方案。

  • 内容

并行程序易于产生 bug 不是什么秘密。编写这种程序是一种挑战,并且在编程过程中悄悄产生的 bug 不容易被发现。许多并行 bug 只有在系统测试、功能测试时才能被发现或由用户发现。到那时修复它们需要高昂的费用 -- 假设能够修复它们 -- 因为它们是如此难于调试。

在本文中,我们介绍了 ConTest,一种用于测试、调试和测量并行程序范围的工具。正如您将很快看到的,ConTest 不是单元测试的取代者,但它是处理并行程序的单元测试故障的一种补充技术。

注意本文中包含了一个 示例软件包 ,一旦理解了有关 ConTest 如何工作的基本知识,您就可以自己使用该软件包进行试验。

为什么单元测试还不够

当 问任何 Java™ 开发者时,他们都会告诉您单元测试是一种好的实践。在单元测试上做适当的投入,随后将得到回报。通过单元测试,能较早地发现 bug 并且能比不进行单元测试更容易地修复它们。但是普通的单元测试方法(即使当彻底地进行了测试时)在查找并行 bug 方面不是很有效。这就是为什么它们能逃到程序的晚期 。

为什么单元测试经常遗漏并行 bug?通常的说法是并行程序(和 bug)的问题在于它们的不确定性。但是对于单元测试目的而言,荒谬性在于并行程序是非常 确定的。下面的两个示例解释了这一点。

无修饰的 NamePrinter

第一个例子是一个类,该类除了打印由两部分构成的名字之外,什么也不做。出于教学目的,我们把此任务分在三个线程中:一个线程打印人名,一个线程打印空格,一个线程打印姓和一个新行。一个包括对锁进行同步和调用 wait()notifyAll() 的成熟的同步协议能保证所有事情以正确的顺序发生。正如您在清单 1 中看到的,main() 充当单元测试,用名字 "Washington Irving" 调用此类:

清单 1. NamePrinter
  1. public class NamePrinter {
  2. private final String firstName;
  3. private final String surName;
  4. private final Object lock = new Object();
  5. private boolean printedFirstName = false;
  6. private boolean spaceRequested = false;
  7. public NamePrinter(String firstName, String surName) {
  8. this.firstName = firstName;
  9. this.surName = surName;
  10. }
  11. public void print() {
  12. new FirstNamePrinter().start();
  13. new SpacePrinter().start();
  14. new SurnamePrinter().start();
  15. }
  16. private class FirstNamePrinter extends Thread {
  17. public void run() {
  18. try {
  19. synchronized (lock) {
  20. while (firstName == null) {
  21. lock.wait();
  22. }
  23. System.out.print(firstName);
  24. printedFirstName = true;
  25. spaceRequested = true;
  26. lock.notifyAll();
  27. }
  28. } catch (InterruptedException e) {
  29. assert (false);
  30. }
  31. }
  32. }
  33. private class SpacePrinter extends Thread {
  34. public void run() {
  35. try {
  36. synchronized (lock) {
  37. while ( ! spaceRequested) {
  38. lock.wait();
  39. }
  40. System.out.print(' ');
  41. spaceRequested = false;
  42. lock.notifyAll();
  43. }
  44. } catch (InterruptedException e) {
  45. assert (false);
  46. }
  47. }
  48. }
  49. private class SurnamePrinter extends Thread {
  50. public void run() {
  51. try {
  52. synchronized(lock) {
  53. while ( ! printedFirstName || spaceRequested || surName == null) {
  54. lock.wait();
  55. }
  56. System.out.println(surName);
  57. }
  58. } catch (InterruptedException e) {
  59. assert (false);
  60. }
  61. }
  62. }
  63. public static void main(String[] args) {
  64. System.out.println();
  65. new NamePrinter("Washington", "Irving").print();
  66. }
  67. }

如果您愿意,您可以编译和运行此类并且检验它是否像预期的那样把名字打印出来。 然后,把所有的同步协议删除,如清单 2 所示:

清单 2. 无修饰的 NamePrinter
  1. public class NakedNamePrinter {
  2. private final String firstName;
  3. private final String surName;
  4. public NakedNamePrinter(String firstName, String surName) {
  5. this.firstName = firstName;
  6. this.surName = surName;
  7. new FirstNamePrinter().start();
  8. new SpacePrinter().start();
  9. new SurnamePrinter().start();
  10. }
  11. private class FirstNamePrinter extends Thread {
  12. public void run() {
  13. System.out.print(firstName);
  14. }
  15. }
  16. private class SpacePrinter extends Thread {
  17. public void run() {
  18. System.out.print(' ');
  19. }
  20. }
  21. private class SurnamePrinter extends Thread {
  22. public void run() {
  23. System.out.println(surName);
  24. }
  25. }
  26. public static void main(String[] args) {
  27. System.out.println();
  28. new NakedNamePrinter("Washington", "Irving");
  29. }
  30. }

这个步骤使类变得完全错误:它不再包含能保证事情以正确顺序发生的指令。但我们编译和运行此类时会发生什么情况呢?所有的事情都完全相同!"Washington Irving" 以正确的顺序打印出来。

此试验的寓义是什么?设想 NamePrinter 以及它的同步协议是并行类。 您运行单元测试 -- 也许很多次 -- 并且它每次都运行得很好。自然地,您认为可以放心它是正确的。但是正如您刚才所看到的,在根本没有同步协议的情况下输出同样也是正确的,并且您可以安全地推断在有很多错误的协议实现的情况下输出也是正确的。因此,当您认为 已经测试了您的协议时, 您并没有真正地 测试它。

现在我们看一下另外的一个例子。

多 bug 的任务队列

下面的类是一种常见的并行实用程序模型:任务队列。它有一个能使任务入队的方法和另外一个使任务出队的方法。在从队列中删除一个任务之前,work() 方法进行检查以查看队列是否为空,如果为空则等待。enqueue() 方法通知所有等待的线程(如果有的话)。 为了使此示例简单,目标仅仅是字符串,任务是把它们打印出来。再一次,main() 充当单元测试。顺便说一下,此类有一个 bug。

清单 3. PrintQueue
  1. import java.util.*;
  2. public class PrintQueue {
  3. private LinkedList<String> queue = new LinkedList<String>();
  4. private final Object lock = new Object();
  5. public void enqueue(String str) {
  6. synchronized (lock) {
  7. queue.addLast(str);
  8. lock.notifyAll();
  9. }
  10. }
  11. public void work() {
  12. String current;
  13. synchronized(lock) {
  14. if (queue.isEmpty()) {
  15. try {
  16. lock.wait();
  17. } catch (InterruptedException e) {
  18. assert (false);
  19. }
  20. }
  21. current = queue.removeFirst();
  22. }
  23. System.out.println(current);
  24. }
  25. public static void main(String[] args) {
  26. final PrintQueue pq = new PrintQueue();
  27. Thread producer1 = new Thread() {
  28. public void run() {
  29. pq.enqueue("anemone");
  30. pq.enqueue("tulip");
  31. pq.enqueue("cyclamen");
  32. }
  33. };
  34. Thread producer2 = new Thread() {
  35. public void run() {
  36. pq.enqueue("iris");
  37. pq.enqueue("narcissus");
  38. pq.enqueue("daffodil");
  39. }
  40. };
  41. Thread consumer1 = new Thread() {
  42. public void run() {
  43. pq.work();
  44. pq.work();
  45. pq.work();
  46. pq.work();
  47. }
  48. };
  49. Thread consumer2 = new Thread() {
  50. public void run() {
  51. pq.work();
  52. pq.work();
  53. }
  54. };
  55. producer1.start();
  56. consumer1.start();
  57. consumer2.start();
  58. producer2.start();
  59. }
  60. }

运行测试以后,所有看起来都正常。作为类的开发者,您很可能感到非常满意:此测试看起来很有用(两个 producer、两个 consumer 和它们之间的能试验 wait 的有趣顺序),并且它能正确地运行。

但是这里有一个我们提到的 bug。您看到了吗?如果没有看到,先等一下;我们将很快捕获它。

回页首

并行程序设计中的确定性

为什么这两个示例单元测试不能测试出并行 bug?虽然原则上线程调度程序可以 在运行的中间切换线程并以不同的顺序运行它们,但是它往往 不进行切换。因为在单元测试中的并行任务通常很小同时也很少,在调度程序切换线程之前它们通常一直运行到结束,除非强迫它(也就是通过 wait())。并且当它确实 执行了线程切换时,每次运行程序时它往往都在同一个位置进行切换。

像我们前面所说的一样,问题在于程序是太确定的:您只是在很多交错情况的一种交错(不同线程中命令的相对顺序)中结束了测试。更多的交错在什么时候试验?当有更多的并行任务以及在并行类和协议之间有更复杂的相互影响时,也就是当您运行系统测试和功能测试时 -- 或当整个产品在用户的站点运行时,这些地方将是暴露出 bug 的地方。

回页首

使用 ConTest 进行单元测试

当进行单元测试时需要 JVM 具有低的确定性,同时是更“模糊的”。这就是要用到 ConTest 的地方。如果使用 ConTest 运行几次 清单 2NakedNamePrinter, 将得到各种结果,如清单 4 所示:

清单 4. 使用 ConTest 的无修饰的 NamePrinter
  1. >Washington Irving (the expected result)
  2. > WashingtonIrving (the space was printed first)
  3. >Irving
  4. Washington (surname + new-line printed first)
  5. > Irving
  6. Washington (space, surname, first name)

注意不需要得到像上 面那样顺序的结果或相继顺序的结果;您可能在看到后面的两个结果之前先看到几次前面的两个结果。但是很快,您将看到所有的结果。ConTest 使各种交错情况出现;由于随机地选择交错,每次运行同一个测试时都可能产生不同的结果。相比较的是,如果使用 ConTest 运行如 清单 1 所示的 NamePrinter ,您将总是得到预期的结果。在此情况下,同步协议强制以正确的顺序执行,所以 ConTest 只是生成合法的 交错。

如果您使用 ConTest 运行 PrintQueue,您将得到不同顺序的结果,这些对于单元测试来说可能是可接受的结果。但是运行几次以后,第 24 行的 LinkedList.removeFirst() 会突然抛出 NoSuchElementException 。bug 潜藏在如下的情形中:

  1. 启动了两个 consumer 线程,发现队列是空的,执行 wait()
  2. 一个 producer 把任务放入队列中并通知两个 consumer。
  3. 一个 consumer 获得锁,运行任务,并把队列清空。然后它释放锁。
  4. 第二个 consumer 获得锁(因为通知了它所以它可以继续向下进行)并试图运行任务,但是现在队列是空的。

这虽然不是此单元测试的常见交错,但上面的场景是合法的并且在更复杂地使用类的时候可能发生这种情况。使用 ConTest 可以使它在单元测试中发生。(顺便问一下,您知道如何修复 bug 吗?注意:用 notify() 取代 notifyAll() 能解决此情形中的问题,但是在其他情形中将会失败!)

回页首

ConTest 的工作方式

ConTest 背后的基本原理是非常简单的。instrumentation 阶段转换类文件,注入挑选的用来调用 ConTest 运行时函数的位置。在运行时,ConTest 有时试图在这些位置引起上下文转换。 挑选的是线程的相对顺序很可能影响结果的那些位置:进入和退出 synchronized 块的位置、访问共享变量的位置等等。通过调用诸如 yield()sleep() 方法来尝试上下文转换。决定是随机的以便在每次运行时尝试不同的交错。使用试探法试图显示典型的 bug。

注 意 ConTest 不知道实际是否已经显示出 bug -- 它没有预期程序将如何运行的概念。是您,也就是用户应该进行测试并且应该知道哪个测试结果将被认为是正确的以及哪个测试结果表示 bug。ConTest 只是帮助显示出 bug。另一方面,没有错误警报:就 JVM 规则而言所有使用 ConTest 产生的交错都是合法的。

正如您看到的一样,通过多次运行同一个测试得到了多个值。实际上,我们推荐整个晚上都反复运行它。然后您就可以很自信地认为所有可能的交错都已经执行过了。

回页首

ConTest 的特性

除了它的基本的方法之外,ConTest 在显示并行 bug 方面引入了几个主要特性:

  • 同步覆盖:在单元测试中极力推荐测量代码覆盖,但是在测试并行程序时使用它,代码覆盖容易产生误导。在前两个例子中,无修饰的 NamePrinter 和多 bug 的 Print Queue,给出的单元测试显示完整的语句覆盖( 除了 InterruptedException 处理)没有显示出 bug。 同步覆盖弥补了此缺陷:它测量在 synchronized 块之间存在多少竞争;也就是说,是否它们做了“有意义的”事情,您是否覆盖了有趣的交错。有关附加信息请参见 参考资料
  • 死锁预防: ConTest 可以分析是否以冲突的顺序嵌套地拥有锁,这表明有死锁的危险。此分析是在运行测试后离线地进行。
  • 调试帮助:ConTest 可以生成一些对并行调试有用的运行时报告:关于锁的状态的报告(哪个线程拥有哪个锁,哪个线程处于等待状态等等),当前的线程的位置的报告和关于最后分配 给变量和从变量读取的值的报告。您也可以远程进行这些查询;例如,您可以从不同的机器上查询服务器(运行 ConTest)的状态。另一个对调试有用的特性可能是重放,它试图重复一个给定运行的交错(不能保证,但是有很高的可能性)。
  • UDP 网络混乱:ConTest 支持通过 UDP(数据报)套接字进行网络通信的域中的并行混乱的概念。 UDP 程序不能依靠网络的可靠性;分组可能丢失或重新排序,它依靠应用程序处理这些情况。与多线程相似,这带来对测试的挑战:在正常环境中,分组往往是按正确的 顺序到达,实际上并没有测试混乱处理功能。ConTest 能够模拟不利的网络状况,因此能够运用此功能并显示它的 bug。

回页首

挑战与未来方向

ConTest 是为 Java 平台创建的。用于 pthread 库的 C/C++ 版本的 ConTest 在 IBM 内部使用,但是不包含 Java 版的所有特性。出于两种原因,用 ConTest 操作 Java 代码比操作 C/C++ 代码简单:同步是 Java 语言的一部分,并且字节码非常容易使用。我们正在开发用于其他库的 ConTest,例如 MPI 库。如果您想要使用 C/C++ 版的ConTest,请与作者联系。硬实时软件对于 ConTest 也是一个问题,因为工具是通过增加延迟而工作。为使用 ConTest,我们正在研究与监视硬实时软件相似的方法,但是在目前我们还不能确定如何克服此问题。

至于将来的方向,我们正在研究发布一种 监听器 体系结构,它将允许我们在 ConTest 上应用基于监听器的工具。使用监听器体系结构将使创建原子数检查器、死锁侦听器和其他分析器以及尝试不必写入有关的基础设施的新的延迟机制成为可能。

回页首

结束语

ConTest 是用于测试、调试和测量并行程序的范围的工具。它由位于以色列海法市的 IBM Research 实验室的研究人员开发,可以 从 alphaWorks 获得 ConTest 的有限制的试用版。如果您有关于 ConTest 的更多问题,请联系作者。

为什么并行测试很困难以及如何使用 ConTest 辅助测试的更多相关文章

  1. 功能测试很low?不能升级到高级测试工程师?

    功能测试很low?不能升级到高级测试工程师? 功能测试很low?功能测试很简单?功能测试就是黑盒测试?功能测试没有技术含量?功能测试工资低?只会功能测试没有竞争力?功能测试这活初中生都可以干?功能测试 ...

  2. 【Android测试】【随笔】与 “58同城” 测试开发交流

    ◆版权声明:本文出自胖喵~的博客,转载必须注明出处. 转载请注明出处:http://www.cnblogs.com/by-dream/p/5384698.html 初衷 一直都有一个这样的想法: 虽然 ...

  3. .net单元测试——常用测试方式(异常模拟、返回值测试、参数测试、数据库访问代码测试)

    最近在看.net单元测试艺术,我也喜欢单元测试,今天介绍一下如何测试异常.如何测试返回值.如何测试模拟对象的参数传递.如何测试数据库访问代码.单元测试框架使用的是NUnit,模拟框架使用的是:Rhin ...

  4. Web渗透测试使用Kali Linux(一)渗透测试概要及环境部署

    渗透测试是利用已经发现的漏洞,采用恶意黑客的惯用手段来尝试对漏洞进行攻击. Kali Linux是BackTrack的进化版,是Linux的衍生版本,专门开发用作渗透测试,其中提供了很多的渗透测试工具 ...

  5. [App Store Connect帮助]六、测试 Beta 版本(3.2)管理测试员:邀请外部测试员

    在您上传至少一个构建版本之后,您可以邀请外部测试员(您组织之外的人员)使用“TestFlight Beta 版测试”来测试您的 App.为了使您的构建版本可用于外部测试,请创建一个群组.添加构建版本, ...

  6. itest(爱测试) 3.3.5 发布,开源敏捷测试管理 & BUG 跟踪管理软件

    v3.3.5 下载地址 :itest下载 itest 简介:查看简介 V3.3.5 有 6个功能增强,2个BUG修复 ,详情如下所述. 用户反馈并强烈要求增强的功能实现:    1: 测试用例管理可线 ...

  7. 今天写了一个可以测试并发数和运行次数的压力测试代码。(Java)

    今天写了一个可以测试并发数和运行次数的压力测试代码 介绍一下为什么会写这么一个工具. 介绍一个这个工具怎么用的. 背景 最近在开发CoapServer端,以及模拟设备侧发送数据调用开发好的CoapSe ...

  8. linux arp攻击解决方法 测试很有效

    公司有台centos服务器中了arp攻击,严重影响业务,测试了很多方法都没解决,机房技术也没法处理. 通过下面方法,可以有效抵挡arp攻击.   1.环境 centos6.4   2.执行 arpin ...

  9. 收集的照片信息都是Excel超链接?批量命名很困难?来试试这个自制的下载器吧!

    项目背景 作为大学的一名班委,经常要制作各种表格.统计各种信息,成为一名合格的"表哥"是一门必修课.其实Excel的文字信息和数字信息的统计和处理还并不算难题,很多信息可以通过问卷 ...

随机推荐

  1. scala与java之间的那些事

    scala与java之间的关系,我认为可以用一句话来开头:scala来源于java,但又高于java. scala的设计者Martin Odersky就是一个JAVA控,这位牛人设计了javac和编写 ...

  2. linux 10 -Bash Shell编程

    二十三. Bash Shell编程:     1.  读取用户变量:     read命令是用于从终端或者文件中读取输入的内建命令,read命令读取整行输入,每行末尾的换行符不被读入.在read命令后 ...

  3. Python赋值魔法技巧

    实验环境: [root@localhost ~]# python -V Python 2.7.5 1.序列解包 多个赋值操作可以同时进行 >>> x,y,z = 1,2,3 > ...

  4. iBatis 简单介绍及基础入门

    iBATIS一词来源于“internet”和“abatis”的组合,是一个由Clinton Begin在2002年发起的开放源代码项目.于2010年6月16号被谷歌托管,改名为MyBatis.是一个基 ...

  5. ubuntu切换到root

    sudo+命令,输入当前用户密码后以root权限执行命令,有时间限制且仅限当前命令. sudo -i,输入当前用户密码后以root权限登录shell,无时间限制.使用exit或logout退出. su ...

  6. centos7下Zookeeper+sheepdog集群搭建

    zookeeper 安装命令 yum install zookeeper -y            (版本:zookeeper.x86_64      3.4.6-1) yum install zo ...

  7. static{}块的作用

    本文转载自: https://www.cnblogs.com/caolaoshi/p/7824748.html static{}块,会且仅会在类被加载时执行一次,多用于定义静态变量或执行静态方法. 什 ...

  8. jquery 初篇

    一.什么是jQuery对象? jQuery 对象就是通过jQuery包装DOM对象后产生的对象. jQuery 对象是 jQuery 独有的. 如果一个对象是 jQuery 对象, 那么它就可以使用  ...

  9. 第一天 格式化操作符 条件、for、while、break、continue语句

    python2和3的区别: 2中的print 不必加括号 3中的print变为函数 要加括号   2中的input不能输入字母(输入的字母被认为是变量,而之前又没定义,所以报错),默认只能计算数字,要 ...

  10. Ubuntu 16.04 NFS搭建

    NFS服务器配置: 1.安装NFS相关包 apt-get install nfs-kernel-server nfs-common # centos 7# yum install nfs-utils ...