編程的樂趣:快速終止!
當軟體出現問題的時候,它應該以一種很容易引起注意的方式馬上終止。這種「快速終止」的方式值得借鑒,我們會在這期專欄里談談這個重要的概念。
一開始,「快速終止」看上去是一種會影響可靠性的不好的實踐——為什麼一個系統在還可以繼續運行的時候要崩潰(或者說終止)?對於這個,我們需要理解,快速終止是和Heisenbugs(對於不易復現bug的一種稱呼)緊密聯繫在一起的。
考慮一下Bohrbugs(對於能夠重現的bug的一種稱呼),它們在一個給定輸入的條件下總是會出現,比如,訪問空指針。這類問題很容易測試、復現並修復。而如今,所有有經驗的程序員應該都面對過這樣的情形:導致崩潰的bug在重啟軟體後就不再出現了。不管花多少時間或努力去重現問題,那個bug就是跟我們捉迷藏。這種bug被稱為Heisenbugs。
花在尋找、修復和測試Heisenbugs上的努力比起Bohrbugs來說,要高出一個數量級。一種避免Heisenbugs的策略是將它們轉化為Bohrbugs。怎麼做呢?預測可能導致Heisenbugs的因素,然後嘗試將它們變成Bohrbugs。是的,這並不簡單,而且也並不是一定可行,但是讓我們來看一個能產生效果的特殊例子。
並發編程是Heisenbugs經常出現的一個典範。我們的例子就是一個Java里和並發相關的問題。在遍歷一個Java集合的時候,一般要求只能通過Iterator的方法對集合進行操作,比如remove()方法。而在遍歷期間,如果有另一個線程嘗試修改底層集合(因為編程時留下的錯誤),那麼底層集合就可能會被破壞(例如,導致不正確的狀態)。
類似這種不正確的狀態會導致不確定的錯誤——假如我們幸運的話(實際上,這很不幸!),程序可以繼續執行而不會崩潰,但是卻給出錯誤的結果。這種bug很難重現和修復,因為這一類的程序錯誤都是不確定的。換句話說,這是個Heisenbug。
幸運的是,Java Iterators會嘗試偵測這種並發修改,並且當發現時,會拋出異常ConcurrentModificationException,而不是等到最後再出錯——那樣也是沒有任何跡象的。換句話說,Java Iterators也遵從了「快速終止」的方法。
如果一個ConcurrentModificationException異常在正式版軟體中發生了呢?根據在Javadoc里對這個異常的說明,它「只應該被用於偵測bug」。換句話說,ConcurrentModificationException只應該在開發階段監聽和修復,而不應該泄漏到正式代碼中。
好吧,如果正式軟體確實發生了這個異常,那它當然是軟體中的bug,應當報告給開發者並修復。至少,我們能夠知道曾經發生過一次針對底層數據結構的並發修改嘗試,而這是軟體出錯的原因(而不是讓軟體產生錯誤的結果,或是以其他現象延後出錯,這樣就很難跟蹤到根本原因)。
「防止崩潰」的途徑就意味著開發健壯的代碼。一個很好的編寫容錯代碼的例子就是使用斷言。很可惜的是,關於斷言的使用有大量不必要的公開爭論。其中主要的批評點是:它在開發版本中使用,而在發布版中卻被關掉的。
不管怎麼樣,這個批評是錯誤的:從來沒有說要用斷言來替代應該放到發布版軟體中的防禦式檢查代碼。例如,斷言不應該用來檢查傳遞給函數的參數是否為空。相應的,應該用一個if語句來檢查這個參數是否正確,否則的話拋出一個異常,或是提前返回,來適合上下文。然而,斷言一般可以用於額外檢查代碼中所作出的假設,這些假設應該一直為真才正常。例如,用一個語句來檢查在進行了入棧操作後,棧應該不是空的(例如,對「不變數」的檢查)。
所以,快速終止,隨時中斷,那麼你就走在開發更加健壯代碼的道路上了。
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive