除錯與工具

執行緒消毒器(ThreadSanitizer, TSan)

/ TSan -> TEE-san /

當兩個執行緒共享資料、而其中至少一個在沒有適當同步的情況下寫它,你就有了資料競爭——而這些是程式設計裡最殘忍的臭蟲之一。它們只在特定時序下出現、常常每數千次執行才一次、常常在你加一個印出或接上除錯器時就消失、還能默默地破壞資料。你需要一個懂得安全共享規則的工具,即使在某次靠運氣沒有任何東西顯眼地壞掉的執行上,也能標出一個競爭。執行緒消毒器就是那個工具。

具體來說,TSan 是 gcc 與 clang 裡的一種消毒器,用 -fsanitize=thread(加 -g)啟用。編譯器對每一次記憶體存取與每一個同步操作(上鎖、解鎖、原子操作、執行緒建立與 join)插樁。在執行期 TSan 追蹤一個「happens-before(發生在前)」關係:哪些存取被同步保證有了順序、哪些沒有。當它看到兩個執行緒存取同一個記憶體位置、其中至少一次是寫入、且沒有任何同步把它們排序,它就回報一個資料競爭——即使在這次特定的執行上那兩次存取碰巧沒有重疊。報告會指名兩次存取連同堆疊追蹤、牽涉的執行緒,以及那塊記憶體在哪裡被配置。它的代價比 ASan 重——大約慢 5 到 15 倍、記憶體用得多很多——而且 TSan 無法與 ASan 併用。

為何重要:資料競爭是非決定性的、出了名地難以重現,所以靠普通除錯去找它們幾乎沒指望;TSan 是藉由分析同步、而非靠臭蟲真的現形來找到它們。誠實的提醒:它只偵測在這次執行中實際碰到的記憶體上的競爭、且只在它插樁過的執行緒上,所以未測到的程式路徑與未插樁的函式庫會藏住競爭;對於它不理解的自訂同步,它偶爾會漏掉、或更罕見地誤標;而作為與記憶體錯誤各自獨立的軸,你為並行跑 TSan 建構、為破壞跑 ASan 建構,而不是一次兩者都跑。一次乾淨的 TSan 執行是無競爭的有力證據,而非證明。

$ gcc -g -fsanitize=thread race.c -o a -lpthread 對兩個沒有互斥鎖、各自做 counter++ 的執行緒,會印出「WARNING: ThreadSanitizer: data race ... Write of size 4 by thread T2 ... Previous write ... by thread T1」,精確指出兩次未同步的存取——即使印出來的 counter 看起來是對的。

即使在一次得出正確數字的執行上,TSan 也回報了 counter++ 上的資料競爭。

TSan 是依同步規則偵測競爭的,所以即使這次執行碰巧得出正確結果,它也會標出一個——這正是它的全部用意。它只涵蓋執行期間碰到的記憶體與執行緒、無法與位址消毒器併用,也可能被它不認得的手寫同步搞糊塗。

又稱
TSan-fsanitize=threaddata-race detector