本節書摘來自華章出版社《多核與gpu程式設計:工具、方法及實踐》一書中的第3章,第3.9節, 作 者 multicore and gpu programming: an integrated approach[阿聯酋]傑拉西莫斯·巴拉斯(gerassimos barlas) 著,張雲泉 賈海鵬 李士剛 袁良 等譯, 更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。
調試多線程應用不僅僅是具備一個能夠管理多線程的調試器。許多現代調試器支援線程的執行和獨立調試,并支援指定線程的斷點、觀察窗等。本節不讨論調試器的具體實作方法。例如,圖3-13展示了ddd——gnu debugger(gdb)前端,它執行代碼清單3-24中公平的讀者–寫者解決方案。在unix/linux中能夠使用ddd和gdb(對于cli困難)的唯一要求是在編譯程式時添加調試支援選項,亦即使用編譯器的-g開關。
多線程程式中的bug通常隻有在與事件的某個精确時序有關的特定環境下才會出現。調試器通過暫停或者降低線程執行速度幹擾了線程時序,使得bug的重制和發現是一項具有挑戰性的工作。最終,發現bug是一項需要經驗和直覺的任務,但是适當地植入程式代碼可以幫助簡化調試工作。
下面的清單中枚舉了程式員為確定一個無bug的多線程應用應該遵循的步驟。
消除多線程應用中bug的第一步當然是不要在最初将其加入到程式中。在編寫代碼之前,一個合理的軟體設計方案是十分關鍵的。
本章研究的經典問題不僅僅具有教學意義。實際應用中遇到的大部分并發問題,或者是這些問題的執行個體,或者可以通過簡單的變換将其歸約到這些問題上。使用本章介紹的解決方案可以避免重新設計一個新方案所帶來的問題。
應用程式應該被修改為支援生成某種可以離線處理的日志或者足迹曆史資訊。這允許收集有關應用程式運作時行為的資訊。
在應用程式中擁有過量的線程會使得生成的日志資訊難于了解。就線程個數來說,将應用程式參數化一般是一個好的設計。通過限制線程數目為1,可以發現與時序無關的bug。将線程數目設定為2或者3,可以降低從日志中抽取資訊的開銷。

通過<code>printf和cout</code>語句來維護執行路勁以及程式狀态是不夠的。在同一時刻使多線程生成控制台(或者檔案)輸出經常會導緻消息的混亂以緻無法解碼。解決方法是将控制台(或者檔案流)作為一個共享資源對待,并且将輸出語句放置到關鍵區中。
一種方式是将調試和追蹤資訊放置到記憶體緩沖區中(稱為足迹緩沖區),并在程式終止時儲存。這是一種有問題的方式,可能導緻程式的錯誤行為。這需要程式(a)正常終止(亦即沒有發生崩潰或者挂起),(b)緩沖區足夠大,能存儲所有生成的資訊,以及(c)緩沖區沒有被記憶體錯誤影響。
一個更好的解決方案是把足迹消息盡可能快地轉儲到控制台中。這種方式也适用于檔案,但是對每一條需要儲存的消息引入了打開和關閉檔案的開銷。否則,如果程式崩潰,可能導緻檔案最近更改及重要資訊的丢失。
為了區分正常的程式輸出和足迹消息,可以利用标準錯誤輸出流。但是當需要進一步處理調試輸出時,例如根據生成消息的線程來過濾消息等處理,又該如何設計?解決方案十分簡單,即通過流重定向。這是*nix和windows環境中的一個通用功能。是以,為了将标準錯誤輸出重定向到trace.log檔案中,可以使用下面的語句:
為所有調試消息打上其生成時間的時間戳也是一個好主意。為了實作這一目的,分辨率在1毫秒或者更低的正常時間函數(例如clock)就不能滿足需求了。需要一個更高分辨率的定時器,這可以通過不同的api實作(更多細節請參考附錄c.2節)。在本節後面的部分假定存在一個稱為<code>hrclock</code>的函數(高分辨率時鐘),它可以傳回一個雙精度類型的時間戳。
代碼清單3-30 展示了一個實作前面讨論的方案的示例。
這個示例程式的關鍵點如下。
附加的代碼片段位于c++的預處理條件塊中(第33~37行,以及第47~49行)。隻需要将第三行注釋掉就可以得到該程式的一個釋出版本。
使用一個全局的互斥量來保證<code>debugmsg</code>函數體位于關鍵區中。
從程式開始執行時開始測量時間。執行個體的時間戳存儲在<code>time0</code>變量中,接下來每一步都從每個計算的時間戳減去它(第15行)。
該程式的一次簡單運作以及對其調試輸出的仔細觀察可以揭示競争條件的工作方式。
最終是一些有關恰當調試程式的警告:應該關閉編譯器優化。優化編譯器可能為了使得執行流更順暢,而改變語句執行順序,或者甚至丢棄在程式中聲明的變量。是以會導緻調試一個優化的程式産生奇怪的結果,例如語句間跳轉。在極端情況下,可能會由于編譯器的優化而導緻一個bug。盡管這是異常的事件,但是還有一些編譯器優化是被辨別為“不安全”的。嘗試将編譯器發揮到極緻的程式員應該確定最終應用程式的行為與未優化的行為是一緻的。
例如,<code>gcc的-ftree-loop-if-conver-stores</code>編譯選項将條件記憶體寫操作變為一個無條件記憶體寫操作。從編譯器手冊中選擇的一個示例如下:
如果數組a是一個共享資源,則兩個版本的代碼都可能會引入競争條件。在後一個版本中,問題可能會被放大。