写一个正确的并行程序要比写顺序执行程序困难。 其原因是并行程序中潜在的风险和错误的种类更多 —— 首先,在一个顺序执行程序中的错误同样会发生在并行程序中;其次,并行程序比顺序执行程序需要关注更多的风险,例如状态的竞争、数据的竞争、死锁、失效的信号以及活锁(livelock)。
同样测试并行程序要比测试顺序执行程序困难。首先,测试并行程序的程序本身就是并行程序;其次,并行程序的错误更难预测和重现。在顺序执行程序中的错误具有确定性。在给定的输入和初始状态下,一个顺序执行程序出错了,那么每次它都会出错,在相同条件下。而一个并行程序出错了,则有可能是一些不确定因素导致的。
由于这点,重现并行程序的错误变得非常困难。不仅错误是随机的,而且现象可能也不确定,甚至在同样的环境下测试也可能不发生错误,也就是说在客户那里每天都发生的错误可能在你的测试实验室中就不会发生。进一步说,试图调试和监控并行程序会引入时间片(timing)和同步的概念,这也有可能阻止错误的发生。在海森堡不确定理论(Heisenberg's uncertainty principle)中,观察一个系统的状态往往会改变它。
所以,基于以上这些令人沮丧的消息,我们应该如何保证并行程序可以正常工作呢?我们使用和其他工程学同样的方法来管理并行程序测试的复杂性 —— 尽量可能地隔离这个复杂性。
构造限制并行交互的程序
我们可以在程序中全部使用公有的和静态的变量。提醒你,这不是一个好主意,但是它确实可行 —— 只是这样做更困难并且程序更脆弱。通过封装,我们可以不必关心所有程序代码就可以分析某部分程序的行为。
同样地,通过把并行交互(concurrent interactions)封装在几个地方,例如,工作流管理器、资源池、工作队列、以及其他的并行对象中。这样会使得分析和测试并行程序变得更简单。一旦并行交互被封装后,你就可以集中精力测试并行机制其自身而不被其他错误所困扰。
并行机制,例如共享的工作队列,经常被作为从一个线程到另一个线程的管道。这些机制中,通常包含了必要的同步机制来保证其中数据的完整性 —— 但是被传入和传出的对象属于应用程序而非工作队列,所以应用程序就有责任负责这些对象的线程安全。你可以使这些对象成为线程安全的(最简单的办法就是使它们不可变(immutable)同时这也是最可靠的办法),但是另一个说法是:使这种不可变性更有效。
有效的不可变的对象(effectively immutable object)是说,这种对象在设计的时候并非不可变的 —— 它们可以具有可变的状态 —— 但是当其他线程访问这样的对象的时候,程序通常认为它们是不可变的。 换句话说,一旦你把一个可变的对象放入一个共享的数据结构中,此时这个对象可以被其他线程所访问时,确保这个对象不会被其他线程重复修改。通过限制主要几个类的可变性(mutability)可以限制潜在的不正确的并行行为的范围。
代码(1)是如何有效地利用不变性(immutability)来大大简化测试的例子。 客户端代码向工作管理器(work manager)提交一个求最大公倍数的请求,计算程序(calculation)被表示为Callable<BigInteger[]>,执行者(Executor)返回一个Future<BigInteger[]>表示计算程序。客户端代码等待Future计算结果。
FactorTask这个类是不可变的,因此是线程安全的,无需额外的并行交互的测试。但是FactorTask返回一个数组,这个数组是可变的。线程间共享可变状态需要进行同步处理,但是由于应用程序代码的结构,因此一旦这个BigInteger的数组被FactorTask返回,它的内容应该是总是不变的,由此,客户端的代码可以在Executor框架中使用"piggyback"技术来隐式地(implicit)进行同步,这样的话,在访问这个数组的时候就无需额外的同步机制。
class FactorTask implements Callable < BigInteger[] > {
private final BigInteger number;
public FactorTask(BigInteger number) {
this .number = number;
}
public BigInteger[] call() throws Exception {
return factorNumber(number);
}
}
Future < BigInteger[] > future = exec.submit( new FactorTask(number));
// do some stuff
BigInteger[] factors = future.get();
这项技术几乎可以被整合到所有的并行机制中,包括Executor, BlockingQueue, 以及ConcurrentMap。 通过把有效的不可变的对象(effectively immutable object)传递进去然后通过callback得到返回的有效的不可变的对象(effectively immutable object),利用这种方法你可以避免许多创建和测试线程安全类的复杂性。
测试并行的“积木”
一旦你把并行交互隔离到一些组件(component)中,你就可以集中精力测试这些组件。由于测试并行代码非常困难,所以你应该花费比测试顺序执行代码更多的时间来测试它。
以下的一些因素是测试并行类的一些最佳实践。
※ 测试是不稳定的 —— 你应该测试更长时间。
※ 测试更多种状态 —— 只是一遍一遍的测试相同的输入和初始状态是没有用的,你应该测试不同的输入数据。
※ 测试更多的交互 —— 通过调整数据输入的时间使线程之间的交互达到不同状态。
※ 增加线程数量 —— 如果线程数量太少可能测试结果也没有什么意义,更多的线程将会造成更多的冲突。
※ 避免引入同步机制 —— 如果在测试程序中引入同步机制,将会影响到并行程序测试的结果。
所有这些听起来就像是一大堆工作要做,而事实也确实如此。但是通过使用一些被广泛应用而且经过充分测试的组件,我们可以大大减少测试并行程序的工作量。而且通过重用已知的组件库,譬如java.util.concurrent包,你可以进一步地减少测试的负担。
---
原文:http://www.theserverside.com/tt/articles/article.tss?l=TestingConcurrent