利用ConTest举办多线程单位测试
副标题#e#
并行措施易于发生 bug 不是什么奥秘。编写这种措施是一种挑战,而且在编程进程中暗暗发生的 bug 不容易被发明。很多并行 bug 只有在系统测试、成果 测试时才气被发明或由用户发明。到当时修复它们需要奋发的用度 — 假设可以或许 修复它们 — 因为它们是如此难于调试。
在本文中,我们先容了 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();
}
}
#p#副标题#e#
假如您愿意,您可以编译和运行此类而且检讨它是否像预期的那样把名字打 印出来。 然后,把所有的同步协议删除,如清单 2 所示:
清单 2. 无修饰的 NamePrinter
#p#分页标题#e#
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();
}
}
#p#分页标题#e#
运行测试今后,所有看起来都正常。作为类的开拓者,您很大概感想很是满 意:此测试看起来很有用(两个 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 方面引入了几个主要特 性:
#p#分页标题#e#
同步包围:在单位测试中积极推荐丈量代码包围,可是在测试并行措施时使 用它,代码包围容易发生误导。在前两个例子中,无修饰的 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 的更多问题,请接洽作者 。
本文配套源码