眾所周知并行程序設計易于產生 bug。更為嚴重的是,往往在開發過程的晚期當這些并行 bug 引起嚴重的損害時才能發現它們并且難于調試它們。即使徹底地對它們進行了調試,常規的單元測試實踐也很可能遺漏并行 bug。在本文中,并行專家 Shmuel Ur 和 Yarden Nir-Buchbinder 解釋了為什么并行 bug 如此難于捕獲并且介紹了 IBM Research 的一種新的解決方案。
并行程序易于產生 bug 不是什么秘密。編寫這種程序是一種挑戰,并且在編程過程中悄悄產生的 bug 不容易被發現。許多并行 bug 只有在系統測試、功能測試時才能被發現或由用戶發現。到那時修復它們需要高昂的費用 -- 假設能夠修復它們 -- 因為它們是如此難于調試。
在本文中,我們介紹了 ConTest,一種用于測試、調試和測量并行程序范圍的工具。正如您將很快看到的,ConTest 不是單元測試的取代者,但它是處理并行程序的單元測試故障的一種補充技術。
注意本文中包含了一個 示例軟件包 ,一旦理解了有關 ConTest 如何工作的基本知識,您就可以自己使用該軟件包進行試驗。
當問任何 Java™ 開發者時,他們都會告訴您單元測試是一種好的實踐。在單元測試上做適當的投入,隨后將得到回報。通過單元測試,能較早地發現 bug 并且能比不進行單元測試更容易地修復它們。但是普通的單元測試方法(即使當徹底地進行了測試時)在查找并行 bug 方面不是很有效。這就是為什么它們能逃到程序的晚期 。
為什么單元測試經常遺漏并行 bug?通常的說法是并行程序(和 bug)的問題在于它們的不確定性。但是對于單元測試目的而言,荒謬性在于并行程序是非常 確定的。下面的兩個示例解釋了這一點。
第一個例子是一個類,該類除了打印由兩部分構成的名字之外,什么也不做。出于教學目的,我們把此任務分在三個線程中:一個線程打印人名,一個線程打印空格,一個線程打印姓和一個新行。一個包括對鎖進行同步和調用 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 以及它的同步協議是并行類。您運行單元測試 -- 也許很多次 -- 并且它每次都運行得很好。自然地,您認為可以放心它是正確的。但是正如您剛才所看到的,在根本沒有同步協議的情況下輸出同樣也是正確的,并且您可以安全地推斷在有很多錯誤的協議實現的情況下輸出也是正確的。因此,當您認為 已經測試了您的協議時,您并沒有真正地 測試它。
現在我們看一下另外的一個例子。
下面的類是一種常見的并行實用程序模型:任務隊列。它有一個能使任務入隊的方法和另外一個使任務出隊的方法。在從隊列中刪除一個任務之前,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(); }} |
運行測試以后,所有看起來都正常。作為類的開發者,您很可能感到非常滿意:此測試看起來很有用(兩個 producer、兩個 consumer 和它們之間的能試驗 wait
的有趣順序),并且它能正確地運行。
但是這里有一個我們提到的 bug。您看到了嗎?如果沒有看到,先等一下;我們將很快捕獲它。
文章來源于領測軟件測試網 http://www.kjueaiud.com/