寫一個(gè)正確的并行程序要比寫順序執(zhí)行程序困難。 其原因是并行程序中潛在的風(fēng)險(xiǎn)和錯(cuò)誤的種類更多 —— 首先,在一個(gè)順序執(zhí)行程序中的錯(cuò)誤同樣會(huì)發(fā)生在并行程序中;其次,并行程序比順序執(zhí)行程序需要關(guān)注更多的風(fēng)險(xiǎn),例如狀態(tài)的競(jìng)爭(zhēng)、數(shù)據(jù)的競(jìng)爭(zhēng)、死鎖、失效的信號(hào)以及活鎖(livelock)。
同樣測(cè)試并行程序要比測(cè)試順序執(zhí)行程序困難。首先,測(cè)試并行程序的程序本身就是并行程序;其次,并行程序的錯(cuò)誤更難預(yù)測(cè)和重現(xiàn)。在順序執(zhí)行程序中的錯(cuò)誤具有確定性。在給定的輸入和初始狀態(tài)下,一個(gè)順序執(zhí)行程序出錯(cuò)了,那么每次它都會(huì)出錯(cuò),在相同條件下。而一個(gè)并行程序出錯(cuò)了,則有可能是一些不確定因素導(dǎo)致的。
由于這點(diǎn),重現(xiàn)并行程序的錯(cuò)誤變得非常困難。不僅錯(cuò)誤是隨機(jī)的,而且現(xiàn)象可能也不確定,甚至在同樣的環(huán)境下測(cè)試也可能不發(fā)生錯(cuò)誤,也就是說在客戶那里每天都發(fā)生的錯(cuò)誤可能在你的測(cè)試實(shí)驗(yàn)室中就不會(huì)發(fā)生。進(jìn)一步說,試圖調(diào)試和監(jiān)控并行程序會(huì)引入時(shí)間片(timing)和同步的概念,這也有可能阻止錯(cuò)誤的發(fā)生。在海森堡不確定理論(Heisenberg's uncertainty principle)中,觀察一個(gè)系統(tǒng)的狀態(tài)往往會(huì)改變它。
所以,基于以上這些令人沮喪的消息,我們應(yīng)該如何保證并行程序可以正常工作呢?我們使用和其他工程學(xué)同樣的方法來管理并行程序測(cè)試的復(fù)雜性 —— 盡量可能地隔離這個(gè)復(fù)雜性。
構(gòu)造限制并行交互的程序
我們可以在程序中全部使用公有的和靜態(tài)的變量。提醒你,這不是一個(gè)好主意,但是它確實(shí)可行 —— 只是這樣做更困難并且程序更脆弱。通過封裝,我們可以不必關(guān)心所有程序代碼就可以分析某部分程序的行為。
同樣地,通過把并行交互(concurrent interactions)封裝在幾個(gè)地方,例如,工作流管理器、資源池、工作隊(duì)列、以及其他的并行對(duì)象中。這樣會(huì)使得分析和測(cè)試并行程序變得更簡(jiǎn)單。一旦并行交互被封裝后,你就可以集中精力測(cè)試并行機(jī)制其自身而不被其他錯(cuò)誤所困擾。
并行機(jī)制,例如共享的工作隊(duì)列,經(jīng)常被作為從一個(gè)線程到另一個(gè)線程的管道。這些機(jī)制中,通常包含了必要的同步機(jī)制來保證其中數(shù)據(jù)的完整性 —— 但是被傳入和傳出的對(duì)象屬于應(yīng)用程序而非工作隊(duì)列,所以應(yīng)用程序就有責(zé)任負(fù)責(zé)這些對(duì)象的線程安全。你可以使這些對(duì)象成為線程安全的(最簡(jiǎn)單的辦法就是使它們不可變(immutable)同時(shí)這也是最可靠的辦法),但是另一個(gè)說法是:使這種不可變性更有效。
有效的不可變的對(duì)象(effectively immutable object)是說,這種對(duì)象在設(shè)計(jì)的時(shí)候并非不可變的 —— 它們可以具有可變的狀態(tài) —— 但是當(dāng)其他線程訪問這樣的對(duì)象的時(shí)候,程序通常認(rèn)為它們是不可變的。 換句話說,一旦你把一個(gè)可變的對(duì)象放入一個(gè)共享的數(shù)據(jù)結(jié)構(gòu)中,此時(shí)這個(gè)對(duì)象可以被其他線程所訪問時(shí),確保這個(gè)對(duì)象不會(huì)被其他線程重復(fù)修改。通過限制主要幾個(gè)類的可變性(mutability)可以限制潛在的不正確的并行行為的范圍。
代碼(1)是如何有效地利用不變性(immutability)來大大簡(jiǎn)化測(cè)試的例子。 客戶端代碼向工作管理器(work manager)提交一個(gè)求最大公倍數(shù)的請(qǐng)求,計(jì)算程序(calculation)被表示為Callable<BigInteger[]>,執(zhí)行者(Executor)返回一個(gè)Future<BigInteger[]>表示計(jì)算程序??蛻舳舜a等待Future計(jì)算結(jié)果。
FactorTask這個(gè)類是不可變的,因此是線程安全的,無需額外的并行交互的測(cè)試。但是FactorTask返回一個(gè)數(shù)組,這個(gè)數(shù)組是可變的。線程間共享可變狀態(tài)需要進(jìn)行同步處理,但是由于應(yīng)用程序代碼的結(jié)構(gòu),因此一旦這個(gè)BigInteger的數(shù)組被FactorTask返回,它的內(nèi)容應(yīng)該是總是不變的,由此,客戶端的代碼可以在Executor框架中使用"piggyback"技術(shù)來隱式地(implicit)進(jìn)行同步,這樣的話,在訪問這個(gè)數(shù)組的時(shí)候就無需額外的同步機(jī)制。
ExecutorService?exec?
=
?
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();
這項(xiàng)技術(shù)幾乎可以被整合到所有的并行機(jī)制中,包括Executor, BlockingQueue, 以及ConcurrentMap。 通過把有效的不可變的對(duì)象(effectively immutable object)傳遞進(jìn)去然后通過callback得到返回的有效的不可變的對(duì)象(effectively immutable object),利用這種方法你可以避免許多創(chuàng)建和測(cè)試線程安全類的復(fù)雜性。
測(cè)試并行的“積木”
一旦你把并行交互隔離到一些組件(component)中,你就可以集中精力測(cè)試這些組件。由于測(cè)試并行代碼非常困難,所以你應(yīng)該花費(fèi)比測(cè)試順序執(zhí)行代碼更多的時(shí)間來測(cè)試它。
以下的一些因素是測(cè)試并行類的一些最佳實(shí)踐。
※?測(cè)試是不穩(wěn)定的 —— 你應(yīng)該測(cè)試更長(zhǎng)時(shí)間。
※?測(cè)試更多種狀態(tài) —— 只是一遍一遍的測(cè)試相同的輸入和初始狀態(tài)是沒有用的,你應(yīng)該測(cè)試不同的輸入數(shù)據(jù)。
※?測(cè)試更多的交互 —— 通過調(diào)整數(shù)據(jù)輸入的時(shí)間使線程之間的交互達(dá)到不同狀態(tài)。
※?增加線程數(shù)量 —— 如果線程數(shù)量太少可能測(cè)試結(jié)果也沒有什么意義,更多的線程將會(huì)造成更多的沖突。
※?避免引入同步機(jī)制 —— 如果在測(cè)試程序中引入同步機(jī)制,將會(huì)影響到并行程序測(cè)試的結(jié)果。
所有這些聽起來就像是一大堆工作要做,而事實(shí)也確實(shí)如此。但是通過使用一些被廣泛應(yīng)用而且經(jīng)過充分測(cè)試的組件,我們可以大大減少測(cè)試并行程序的工作量。而且通過重用已知的組件庫,譬如java.util.concurrent包,你可以進(jìn)一步地減少測(cè)試的負(fù)擔(dān)。
---
原文:http://www.theserverside.com/tt/articles/article.tss?l=TestingConcurrent