快捷搜索:

使用 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 queue = new LinkedList();

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 潜藏在如下的情形中:

启动了两个 consumer 线程,发明行列步队是空的,履行 wait()。

一个 producer 把义务放入行列步队中并看护两个 consumer。

一个 consumer 得到锁,运行义务,并把行列步队清空。然后它开释锁。

第二个 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 的更多问题,请联系作者。

下载

描述

名字

大年夜小

下载措施

Presentation

j-contestsynch-pres.zip

96KB

HTTP

Sample code

j-contestexamples.zip

8KB

HTTP

关于下载措施的信息

Get Adobe® Reader®

参考资料

进修

您可以参阅本文在 developerWorks 举世站点上的 英文原文

Multithreaded Java program test generation(Orit Edelstein、Eitan Farchi、Evgeny Goldin、Yarden Nir、Gil Ratsaby、Shmuel Ur;IBM Systems Journal, 2002 年 11 月):ConTest 的作者对 ConTest 进行了深入评论争论。

Framework for testing multi-threaded Java programs (Orit Edelstein、Eitan Farchi、Evgeny Goldin、Yarden Nir、Gil Ratsaby、Shmuel Ur; Concurrency and Computation: Practice and Experience, 2003 年 2 月):先容作为新的测试框架的 ConTest。

"Java theory and practice: Concurrency in JDK 5.0"(Brian Goetz,developerWorks,2004 年 11 月):进修 JDK 5.0 中对并行的支持是若何有助于更快更轻易地掩护代码。

"Java 理论与实践: JDK 5.0 中更机动、更具可伸缩性的锁定机制" (Brian Goetz,developerWorks,2004 年 10 月):评论争论新的锁举措措施若何支持在 JDK 5.0 中进行高机能利用法度榜样开拓。包括对原子变量的解释。

"Java 理论与实践: 修复 Java 内存模型,第 1 部分" and 第 2 部分(Brian Goetz,developerWorks,2004 年 2月/3月):解释最初的 Java 平台内存模型若何破坏并行法度榜样设计以及 JDK 5.0 做了哪些事情来修补这种侵害。

"Diagnosing Java Code: The Orphaned Thread bug pattern"(Eric Allen,developerWorks,2001 年 8 月):看一看紧张的并行 bug 模式。

"关注机能: 等待透露"(Jack Shirazi and Kirk Pepperdine, developerWorks,2005 年 1 月):演示若何发明等待破绽,它在并行法度榜样中常常呈现。

"Double-checked locking and the Singleton pattern"(Peter Haggar,developerWorks,2002 年 5 月):解释为什么不应应用双层反省锁。

"Classworking 对象箱: 用 Hansel 和 Gretel 覆盖代码"(Dennis Sosnoski,developerWorks,2005 年 2 月):对代码覆盖技巧的先容。

"追求代码质量: 不要被覆盖申报所迷惑"(Andrew Glover,developerWorks,2006 年1 月):评论争论应用代码覆盖丈量作为代码质量唆使的一些缺陷。

Java 技巧专区 稀有百篇关于 Java 编程各方面的文章。

得到产品和技巧

ConTest:从 IBM alphaWorks 下载免费试用版。.

评论争论

经由过程介入 developerWorks blogs 加入 developerWorks 社区。

作者简介

Yarden 得到 Technion Technological Institute 的谋略机科学学士学位和海法大年夜学的哲学硕士学位。从 2000 年以来,他不停在以色列海法的 IBM Research Lab 事情,从事并行测试措施和对象的钻研事情。

Shmuel Ur 博士是 IBM 的主要发现家和位于以色列海法的 IBM Research Lab 的钻研科学家。他的事情领域是软件测试并主要致力于覆盖和多线程法度榜样的测试。Shmuel 在 Technion Technological Institute 和海法大年夜学讲授软件测试课程。他在 1994 年从卡内基梅隆大年夜学得到算法优化和组合数学的博士学位,从以色列理工学院得到理学学士和硕士学位。Shmuel 已经在硬件测试、人工智能、算法、软件测试和多线程法度榜样测试等领域出版了著作。他是一个测试多线程利用法度榜样的钻研小组 PADTAD 和 IBM Verification Conference 的主席。

您可能还会对下面的文章感兴趣: