一個BUG

解決了一個很隱蔽的BUG,就在剛纔。

寫了一個類,其對象實例需在多線程中使用,因此打算把互斥機制封裝在類的內部。

於是在類裏定義了一個boost::mutex類型的變量,取名叫“mutex_”

在成員函數中使用的時候,用這個mutex_對象初始化一個局部鎖:

mutex::scoped_lock lock(mutex_);

然後寫了個單元測試,創建三個線程,每個線程調用對象的某成員函數1000次,通過觀察對象的最終的狀態來確定其行爲跟我想要的是否一致。

單元測試順利通過。

於是繼續做其它事情,寫測試用例,編碼,編譯,測試……如此重複。突然,在測一個新的東西時,卻發現前面的這個測試失敗了一次。再次運行這個測試,卻又成功了,又多次運行,還是成功。

隱約覺得這裏邊可能隱藏着大問題,十有八九是我對互斥體的使用上還是沒考慮周到,結果導致了競爭條件。

可問題是,唯一的一次失敗總是不可重現。咋辦?這是首要的問題,一定要想個辦法讓它容易重現,最好是“一定失敗”,這樣才能使問題的原因更容易查找。

曾考慮讓每個線程在每調用完一次被測函數後,執行一次this_thread::yield();讓出CPU所有權。隨即又打消了這個念頭,因爲直觀看來,這隻會使競爭條件更難以暴露。於是,採用更簡單的方式,把調用次數從1000次提到一萬次,還是沒有重現,10萬次,還是不行,100萬,也不行。

咋這麼彆扭呢,再試一下1000萬吧_這次行了,測試失敗了。又將測試運行了10多次,失敗率100%。

從測試程序的輸出來看,每次被測對象的狀態都不一樣,有各式各樣的取值,這種不確定性更說明競爭條件存在的可能性比較大。

是不是CppUnit並行執行同一個test suite中的各個test case呢?到網上找了一下,沒找到有意義的資料。回頭想想應該不會,要是那樣的話,同一個test suite中,如果多個test case共享使用測試類中定義的成員變量,豈不是也要使用mutex來寫了?在測試類中定義成員變量並在測試函數中訪問的情況很多,但沒見過誰在動不動就在測試類中定義互斥體,所以應該不會。

怎麼辦?看代碼吧。簡單的幾行,自認爲即使在拋出異常的情況下也是線程安全的。究竟是怎麼回事呢?沒辦法,再仔細看。結果一下子看出來了。。。

原來,是一點小小的疏忽,把

mutex::scoped_lock lock(mutex_);

誤寫成了:

mutex::scoped_lock lock(mutex);

馬上改回來。好了,現在成功率100%。

又把次數改成1億次,仍然OK——就是測試時間不可忍受,於是改回了1000萬,後來又試着改成了500萬,發現在寫錯的情況下,重現率仍然100%,就那樣放着了。

轉念一想還是不對,誤將變量名寫成類名,編譯器爲啥不報錯呢?難道又是模板實例化及模板參數推導過程中的那些微妙的規則在作怪?仔細一想,自己也笑了,這一個“疏忽”,含義可大不一樣了,mutex::scoped_lock lock(mutex_);是定義一個局部鎖對象,用成員變量mutex_來初始化它;而mutex::scoped_lock lock(mutex);只是在局部聲明瞭一個函數(返回類型爲mutex::scoped_lock,參數類型爲mutex)——聲明一個函數不會產生任何目標代碼,也就是說,實際上沒有任何互斥發生。

爲什麼原來次數少的時候那麼難以重現呢。我想原因無非是次數太少,實際上又沒有互斥,調用一個小函數1000次,以現在的CPU速度,一個線程在一次時間片中估計就搞定了,於是三個線程實際上還是串行的。後來次數增加了,線程或許偶爾被切換一次,也碰巧競爭條件沒有發作。

bug解決了,能說明啥呢?1000萬?這這個數字能算啥呢?硬件會越來越快,所以它根本沒有任何參考價值。唉,要不怎麼說解bug需要經驗、方法,再加一點運氣呢。

不過,我還是慶幸自己寫了那個單元測試,更感謝上帝讓它失敗了那一次,也慶幸自己能夠抓住不放,最終把原因揪了出來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章