`

使用 ConTest 进行多线程单元测试

 
阅读更多

为什么并行测试很困难以及如何使用 ConTest 辅助测试

众所周知并行程序设计易于产生 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
public class NamePrinter {
   private final String firstName;
   private final String surName;
   private final Object lock = new Object();
   private boolean printedFirstName = false;
   private boolean spaceRequested = false;
   public NamePrinter(String firstName, String surName) {
      this.firstName = firstName;
      this.surName = surName;
   }
   public void print() {
      new FirstNamePrinter().start();
      new SpacePrinter().start();
      new SurnamePrinter().start();
   }
   private class FirstNamePrinter extends Thread {
      public void run() {
         try {
            synchronized (lock) {
               while (firstName == null) {
                  lock.wait();
               }
               System.out.print(firstName);
               printedFirstName = true;
               spaceRequested = true;
               lock.notifyAll();
            }
         } catch (InterruptedException e) {
            assert (false);
         }
      }
   }
   private class SpacePrinter extends Thread {
      public void run() {
         try {
            synchronized (lock) {
               while ( ! spaceRequested) {
                  lock.wait();
               }
               System.out.print(' ');
               spaceRequested = false;
               lock.notifyAll();
            }
         } catch (InterruptedException e) {
            assert (false);
         }
      }
   }
   private class SurnamePrinter extends Thread {
      public void run() {
         try {
            synchronized(lock) {
               while ( ! printedFirstName || spaceRequested || surName == null) {
                  lock.wait();
               }
               System.out.println(surName);
            }
         } catch (InterruptedException e) {
            assert (false);
         }
      }
   }
   public static void main(String[] args) {
      System.out.println();
      new NamePrinter("Washington", "Irving").print();
   }
}

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

清单 2. 无修饰的 NamePrinter
public class NakedNamePrinter {
   private final String firstName;
   private final String surName;
   public NakedNamePrinter(String firstName, String surName) {
      this.firstName = firstName;
      this.surName = surName;
      new FirstNamePrinter().start();
      new SpacePrinter().start();
      new SurnamePrinter().start();
   }
   private class FirstNamePrinter extends Thread {
      public void run() {
         System.out.print(firstName);
      }
   }
   private class SpacePrinter extends Thread {
      public void run() {
         System.out.print(' ');
      }
   }
   private class SurnamePrinter extends Thread {
      public void run() {
         System.out.println(surName);
      }
   }
   public static void main(String[] args) {
      System.out.println();
      new NakedNamePrinter("Washington", "Irving");
   }
}

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

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

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

多 bug 的任务队列

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

清单 3. PrintQueue
import java.util.*;
public class PrintQueue {
   private LinkedList<String> queue = new LinkedList<String>();
   private final Object lock = new Object();
   public void enqueue(String str) {
      synchronized (lock) {
         queue.addLast(str);
         lock.notifyAll();
      }
   }
   public void work() {
      String current;
      synchronized(lock) {
         if (queue.isEmpty()) {
            try {
               lock.wait();
            } catch (InterruptedException e) {
               assert (false);
            }
         }
         current = queue.removeFirst();
      }
      System.out.println(current);
   }
   public static void main(String[] args) {
      final PrintQueue pq = new PrintQueue();
      Thread producer1 = new Thread() {
         public void run() {
            pq.enqueue("anemone");
            pq.enqueue("tulip");
            pq.enqueue("cyclamen");
         }
      };
      Thread producer2 = new Thread() {
         public void run() {
            pq.enqueue("iris");
            pq.enqueue("narcissus");
            pq.enqueue("daffodil");
         }
      };
      Thread consumer1 = new Thread() {
         public void run() {
            pq.work();
            pq.work();
            pq.work();
            pq.work();
         }
      };
      Thread consumer2 = new Thread() {
         public void run() {
            pq.work();
            pq.work();
         }
      };
      producer1.start();
      consumer1.start();
      consumer2.start();
      producer2.start();
   }
}

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

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

 

并行程序设计中的确定性

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

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

使用 ConTest 进行单元测试

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

清单 4. 使用 ConTest 的无修饰的 NamePrinter
>Washington Irving (the expected result)
> WashingtonIrving (the space was printed first)
>Irving
 Washington (surname + new-line printed first)
> Irving
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 产生的交错都是合法的。

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

分享到:
评论

相关推荐

    concurrent 多线程 教材

    26 使用 ConTest 进行多线程单元测试.mht 27 实现非阻塞套接字的一种简单方法.mht 28 基于事件的NIO多线程服务器.mht 29 驯服 Tiger 并发集合.mht 30 Java5 多线程实践.mht 31 Java 理论与实践 并发集合类.mht ...

    Southeastern European Regional Programming Contest 2007 测试数据

    【标题】"Southeastern European Regional Programming Contest 2007 测试数据"是指一场编程竞赛的测试数据集,这是2007年东南欧区域编程比赛的一部分。在这场比赛中,参赛者需要解决一系列的算法问题,而这些测试...

    poj测试数据之[Southwestern European Regional Contest 1996]

    poj上的测室数据,需要的下载........

    2019 Multi-University Training Contest 4(2019hdu多校第六场数据与标程)rar

    “多校6题面”是比赛的核心,它包含了本次比赛的所有题目,每道题目都设计得既有趣又有挑战性,旨在测试选手们的逻辑思维、算法应用和创新解决问题的能力。这些题目通常涵盖排序、图论、动态规划、字符串处理等多种...

    DPA Contest V2

    与前一版本DPA contest v1不同,后者是针对ASIC(专用集成电路)上的数据加密标准(Data Encryption Standard,简称DES)实施的攻击竞赛,而DPA Contest V2则专注于更现代、更复杂的AES算法,并在FPGA平台上进行。...

    ConTest-1.0.8

    《ConTest-1.0.8:对抗Win32/Conficker蠕虫的利器》 在信息技术领域,安全始终是不容忽视的重要环节。...在数字化世界中,了解并使用像ConTest这样的专业工具,将有助于保护我们的数据和网络资源免受侵害。

    测试作业测试作业测试作业

    它涉及到多个阶段,从单元测试到系统测试,再到最终的验收测试。在这些阶段中,开发者和测试人员会逐步验证软件的各个组件和整体功能。 首先,让我们来了解一下测试的主要类型。单元测试是针对软件中的最小可测试...

    Ulm Local Contest1996-1999

    【标题】"Ulm Local Contest1996-1999" 涉及的是一场持续四年的算法竞赛,这个系列比赛是ACM(国际计算机科学联盟)International Collegiate Programming Contest(ICPC)的一部分。ICPC是全球范围内极具影响力的大...

    一个简单的Java并发系统动态测试工具.zip

    Java并发系统动态测试工具是一个强大的资源,用于检查和分析多线程程序在运行时的行为。这个工具基于Java语言,能够帮助开发者深入理解并发程序的工作原理,发现并修复潜在的线程安全问题,如竞态条件、死锁和活锁等...

    2008 Benelux Algorithm Programming Contest.rar

    在这个压缩包中,"2008 Benelux Algorithm Programming Contest"很可能是包含了上述所有内容的主文件夹,里面可能细分有各个题目、对应的测试数据集以及官方解答的子文件。 学习和研究这个竞赛的题目和解答,不仅...

    高一英语必修3 Unit3单元测试题及答案精选.doc

    4. 测试技巧:完成此类单元测试,学生需要掌握时间管理,熟悉题型,以及能够迅速而准确地在选项中找到正确答案,这对提高考试成绩至关重要。 总结:这个高一英语必修3 Unit3单元测试题主要测试学生的语法知识、词汇...

    2014 Multi-University Training Contest 2(标程+数据)

    "2014Mult Contest2" 指代了比赛的年份和编号,"标程"表示提供了示例解决方案,"数据"则意味着有用于测试代码的数据集,而"2014多校"强调了这是跨多所大学的一次活动。 在这个压缩包中,参赛者可以找到以下知识点:...

    有2019 Multi-University Training Contest 9,hdu多校第9场的题解.zip

    《2019 Multi-University Training Contest 9 HDU多校第9场题解解析》 在编程竞赛的世界中,ACM(国际大学生程序设计竞赛)是一项备受瞩目的活动,它旨在培养学生的算法设计、问题解决和编程能力。2019年,多所大学...

    USACO 2024 January Contest, BronzeProblem 3. Balancing Bacteria

    USACO 2024 January Contest, BronzeProblem 3. Balancing Bacteria

    UMAP Journal 38.2 2017 ICM Contest

    Turn theory into practice by entering COMAP's Mathematical Contest in Modeling (MCM). The study of mathematics as a subject in its own right may have started with Pythagoras, but people have been ...

Global site tag (gtag.js) - Google Analytics