寫一個正確的并行程序要比寫順序執行程序困難。 其原因是并行程序中潛在的風險和錯誤的種類更多 —— 首先,在一個順序執行程序中的錯誤同樣會發生在并行程序中;其次,并行程序比順序執行程序需要關注更多的風險,例如狀態的競爭、數據的競爭、死鎖、失效的信號以及活鎖(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)進行同步,這樣的話,在訪問這個數組的時候就無需額外的同步機制。
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();
這項技術幾乎可以被整合到所有的并行機制中,包括Executor, BlockingQueue, 以及ConcurrentMap。 通過把有效的不可變的對象(effectively immutable object)傳遞進去然后通過callback得到返回的有效的不可變的對象(effectively immutable object),利用這種方法你可以避免許多創建和測試線程安全類的復雜性。
測試并行的“積木”
一旦你把并行交互隔離到一些組件(component)中,你就可以集中精力測試這些組件。由于測試并行代碼非常困難,所以你應該花費比測試順序執行代碼更多的時間來測試它。
以下的一些因素是測試并行類的一些最佳實踐。
※?測試是不穩定的 —— 你應該測試更長時間。
※?測試更多種狀態 —— 只是一遍一遍的測試相同的輸入和初始狀態是沒有用的,你應該測試不同的輸入數據。
※?測試更多的交互 —— 通過調整數據輸入的時間使線程之間的交互達到不同狀態。
※?增加線程數量 —— 如果線程數量太少可能測試結果也沒有什么意義,更多的線程將會造成更多的沖突。
※?避免引入同步機制 —— 如果在測試程序中引入同步機制,將會影響到并行程序測試的結果。
所有這些聽起來就像是一大堆工作要做,而事實也確實如此。但是通過使用一些被廣泛應用而且經過充分測試的組件,我們可以大大減少測試并行程序的工作量。而且通過重用已知的組件庫,譬如java.util.concurrent包,你可以進一步地減少測試的負擔。
---
原文:http://www.theserverside.com/tt/articles/article.tss?l=TestingConcurrent