Helgrind:螺纹错误检测器
目錄
7.1。概觀7.2。檢測到的錯誤:POSIX pthread API的濫用7.3。檢測到的錯誤:鎖定排序不一致7.4。檢測到的錯誤:數據競賽要使用此工具,必須--tool=helgrind在Valgrind命令行上指定?。
7.1。概觀
Helgrind是一個Valgrind工具,用于檢測使用POSIX pthreads線程圖元的C,C ++和Fortran程序中的同步錯誤。
POSIX pthreads中的主要抽象是:一組共享公共地址空間,線程創建,線程加入,線程退出,互斥(鎖),條件變量(線程間通知),讀寫器鎖,自旋鎖,信號量的線程和障礙。
Helgrind可以檢測三類錯誤,這些錯誤在接下來的三個部分將詳細討論:
POSIX pthreads API的濫用。
鎖定訂購問題引起的潛在死鎖。
數據競賽 - 無需適當鎖定或同步即可訪問內存。
像這樣的問題常常導致不可再現的,與時間相關的崩潰,僵局和其他不當行為,并且可能難以通過其他方式找到。
Helgrind知道所有的pthread抽象,并盡可能準確地跟蹤它們的效果。在x86和amd64平臺上,它了解并部分處理使用LOCK指令前綴引起的隱式鎖定。在PowerPC / POWER和ARM平臺上,它部分處理由加載鏈接和存儲條件指令對引起的隱式鎖定。
當您的應用程序僅使用POSIX pthreads API時,Helgrind效果最佳。但是,如果要使用自定義線程原語,可以使用定義的ANNOTATE_*宏來描述Helgrind的行為?helgrind.h。
以下是?關于如何從Helgrind獲得最佳效果的提示和提示的部分。
然后有一個命令行選項的?摘要。
最后,?對Helgrind可以改進的領域進行簡要總結。
7.2。檢測到的錯誤:POSIX pthread API的濫用
Helgrind攔截了許多POSIX pthread函數的調用,因此能夠報告各種常見問題。盡管這些都是無人喜好的錯誤,但是他們的存在可能會導致程序行為的不確定性以及難以發現的錯誤。檢測到的錯誤是:
-
解鎖無效互斥體
-
解鎖未鎖定的互斥量
-
解鎖由不同線程持有的互斥體
-
破壞無效或鎖定的互斥體
-
遞歸地鎖定非遞歸互斥體
-
釋放包含鎖定互斥體的內存
-
將mutex參數傳遞給期望讀寫器鎖參數的函數,反之亦然
-
當POSIX pthread函數失敗并且必須處理的錯誤代碼時
-
當線程仍然保持鎖定鎖定時退出
-
調用pthread_cond_wait?與未鎖定的互斥,無效的互斥體,或者一個由不同的線程鎖定
-
條件變量及其相關互斥體之間的綁定不一致
-
pthread屏障的無效或重復初始化
-
線程仍在等待的pthread屏障的初始化
-
破壞從未初始化的pthread屏障對象,或者線程仍在等待
-
等待未初始化的pthread屏障
-
對于Helgrind攔截的所有pthread函數,報告錯誤以及堆棧跟蹤,如果系統線程庫例程返回錯誤代碼,即使Helgrind本身沒有檢測到錯誤
檢查互斥體的有效性通常也適用于讀寫器鎖。
還報告了各種這種不可能發生的事件。這些通常表示系統線程庫中的錯誤。
報告的錯誤總是包含一個主堆棧跟蹤,指示檢測到錯誤的位置。它們還可能包含輔助堆疊軌跡,以提供其他信息。特別是與mutex相關的大多數錯誤也會告訴您互斥體最初來到Helgrind的注意事項(“?was first observed at”部分),因此您有機會找出它所指的互斥體。例如:
線程#1解鎖了鎖定在0x7FEFFFA90的鎖定在0x4C2408D:pthread_mutex_unlock(hg_intercepts.c:492)通過0x40073A:近_main(tc09_bad_unlock.c:27)由0x40079B:main(tc09_bad_unlock.c:50)首先觀察到鎖定在0x7FEFFFA90在0x4C25D01:pthread_mutex_init(hg_intercepts.c:326)通過0x40071F:近_main(tc09_bad_unlock.c:23)由0x40079B:main(tc09_bad_unlock.c:50)Helgrind有一種總結線程身份的方法,正如您在這里看到的文本“?Thread #1”。這就是說,它可以談論線程和線程,而不會壓倒你的細節。有關?解釋錯誤消息的更多信息,請參閱?下文。
7.3。檢測到的錯誤:鎖定排序不一致
在本節中,通常來說,“獲取”鎖只是意味著鎖定該鎖,并且“釋放”鎖定裝置以將其解鎖。
Helgrind監視線程獲取鎖定的順序。這允許它檢測可能由形成鎖的周期而產生的潛在死鎖。檢測這種不一致是有用的,因為雖然實際的死鎖相當明顯,但在測試期間可能永遠不會發現潛在的死鎖,并且可能導致難以診斷的在役故障。
這樣一個問題的最簡單的例子如下。
-
想象一下,一些共享資源R,無論什么原因,都被兩個鎖L1和L2保護,這兩個鎖必須在訪問R時都被保持。
-
假設一個線程獲取L1,然后是L2,然后繼續訪問R.這意味著程序中的所有線程都必須按照L1和L2的順序獲取兩個鎖。不這樣做會危害到僵局。
-
如果兩個線程(稱為T1和T2)都要訪問R,則會發生死鎖。假設T1首先獲取L1,并且T2首先獲取L2。然后T1嘗試獲取L2,T2嘗試獲取L1,但是這些鎖都已經被保持。所以T1和T2變得僵局。
Helgrind建立了一個有針對性的圖表,指示過去獲取鎖的順序。當一個線程獲取一個新的鎖時,圖表被更新,然后檢查它是否現在包含一個循環。循環的存在表明在循環中涉及鎖的潛在的死鎖。
一般來說,Helgrind將選擇兩個涉及循環的鎖,并向您展示他們的采購訂單是如何變得不一致的。它通過顯示首先定義排序的程序點和稍后違反的程序點來實現。這是一個簡單的例子,只涉及兩個鎖:
線程#1:鎖定順序“0x7FF0006D0在0x7FF0006A0之前”被違反觀察(不正確)的順序是:獲取鎖定在0x7FF0006A0在0x4C2BC62:pthread_mutex_lock(hg_intercepts.c:494)0x400825:main(tc13_laog1.c:23)其次是以后獲取鎖定在0x7FF0006D0在0x4C2BC62:pthread_mutex_lock(hg_intercepts.c:494)通過0x400853:main(tc13_laog1.c:24)通過獲取鎖定在0x7FF0006D0建立所需的順序在0x4C2BC62:pthread_mutex_lock(hg_intercepts.c:494)通過0x40076D:main(tc13_laog1.c:17)隨后稍后獲取鎖定在0x7FF0006A0在0x4C2BC62:pthread_mutex_lock(hg_intercepts.c:494)由0x40079B:main(tc13_laog1.c:18)當循環中有兩個以上的鎖時,錯誤同樣嚴重。然而,目前,Helgrind沒有顯示所涉及到的鎖,有時是因為該信息不可用,而且也避免了信息的泛濫。例如,著名的“餐飲哲學家”問題的天真實施涉及五個鎖的循環(見helgrind/tests/tc14_laog_dinphils.c)。在這種情況下,Helgrind已經發現,所有5位哲學家都可以同時拿起左叉,然后在等待拾取正確的叉子時死鎖。
線程#6:鎖定順序“0x8049A00之前0x8049A00”被違反觀察(不正確)的順序是:獲取鎖定在0x8049A00在0x40085BC:pthread_mutex_lock(hg_intercepts.c:495)by 0x80485B4:dine(tc14_laog_dinphils.c:18)由0x400BDA4:mythread_wrapper(hg_intercepts.c:219)by 0x39B924:start_thread(pthread_create.c:297)通過0x2F107D:clone(clone.S:130)其次是以后獲得鎖定在0x80499A0在0x40085BC:pthread_mutex_lock(hg_intercepts.c:495)by 0x80485CD:dine(tc14_laog_dinphils.c:19)由0x400BDA4:mythread_wrapper(hg_intercepts.c:219)by 0x39B924:start_thread(pthread_create.c:297)通過0x2F107D:clone(clone.S:130)7.4。檢測到的錯誤:數據競賽
當兩個線程訪問共享內存位置,而不使用合適的鎖或其他同步來確保單線程訪問時,數據競賽發生或可能發生。這種丟失的鎖定可能導致模糊的與時間相關的錯誤。確保計劃無競爭力是線程編程的核心難題之一。
可靠地檢測種族是一個困難的問題,Helgrind的大部分內部都專門處理它。我們從一個簡單的例子開始。
7.4.1。簡單數據競賽
關于比賽的最簡單的例子如下。在這個程序中,不可能知道var在程序結束時的價值是什么。是2嗎?還是1?
#include <pthread.h>int var = 0;void * child_fn(void * arg){VAR ++; / *相對于父對象沒有保護* / / *這是第6行* /返回NULL; }int main(void){pthread_t小孩pthread_create(&child,NULL,child_fn,NULL);VAR ++; / *相對于小孩沒有保護* / / *這是第13行* /pthread_join(child,NULL);返回0; }問題是var兩個線程同時停止更新。正確的程序將var使用鎖類型進行?保護pthread_mutex_t,這是在每次訪問之前獲取的,然后釋放。Helgrind的這個程序的輸出是:
線程#1是程序的根線程序線程#2已創建在0x511C08E:克隆(在/lib64/libc-2.8.so)由0x4E333A4:do_clone(在/lib64/libpthread-2.8.so)by 0x4E33A30:pthread_create @@ GLIBC_2.2.5(在/lib64/libpthread-2.8.so中)通過0x4C299D4:pthread_create @ *(hg_intercepts.c:214)由0x400605:main(simple_race.c:12)線程#1在0x601038讀取大小4期間的可能數據競爭 鎖定舉行:無在0x400606:main(simple_race.c:13)這與以前由線程#2寫入的大小4沖突 鎖定舉行:無在0x4005DC:child_fn(simple_race.c:6)by 0x4C29AFF:mythread_wrapper(hg_intercepts.c:194)通過0x4E3403F:start_thread(在/lib64/libpthread-2.8.so)由0x511C0CC:克隆(在/lib64/libc-2.8.so)位置0x601038是全局var“var”內的0個字節 在simple_race.c中聲明:3這是一個非常簡單的錯誤的細節。最后一個子句是主錯誤消息。它說,由于讀取大小為4(字節),0x601038,這是var在程序main中第13行功能發生的地址,導致競爭。
消息的兩個重要部分是:
-
Helgrind顯示錯誤的兩個堆棧跟蹤,而不是一個。根據定義,比賽涉及兩個訪問相同位置的不同線程,結果取決于兩個線程的相對速度。
第一個堆棧跟蹤文本“?Possible data race during read of size 4 ...”,第二個跟蹤跟隨文本“?This conflicts with a previous write of size 4 ...”。Helgrind通常能夠顯示比賽中涉及的兩個訪問。其中至少有一個將是一個寫入(因為兩個并發的,不同步的讀取是無害的),并且它們當然是來自不同的線程。
通過在兩個位置檢查您的程序,您應該能夠至少了解問題的根本原因。對于每個位置,Helgrind顯示在訪問時保持的一組鎖。這通常會使得明確哪個線程(如果有的話)無法執行所需的鎖定。在這個例子中,兩個線程都不會在訪問期間保持鎖定。
-
對于在全局或堆棧變量上發生的種族,Helgrind會嘗試識別變量的名稱和定義點。因此文本“?Location 0x601038 is 0 bytes inside global var "var" declared at simple_race.c:3”。
一旦Helgrind的程序啟動并運行,顯示堆棧和全局變量的名稱就不會運行時間。但是,它需要Helgrind在程序啟動時花費相當多的時間和內存來讀取相關的調試信息。因此,默認情況下禁用此功能。要啟用它,您需要--read-var-info=yes選擇Helgrind。
以下部分更詳細地說明了Helgrind的種族檢測算法。
7.4.2。Helgrind的種族檢測算法
大多數程序員根據線程庫(POSIX Pthreads)提供的基本功能(線程創建,線程加入,鎖定,條件變量,信號量和障礙)來考慮線程編程。
使用這些功能的效果是對存儲器訪問可能發生的順序施加約束。這個暗示的順序通常被稱為“發生之前的關系”。一旦了解了之前的關系,很容易看出Helgrind如何在您的代碼中找到種族。幸運的是,之前的關系本身很容易理解,本身就是推理并行程序行為的有用工具。我們現在簡單介紹一下這個例子。
首先考慮以下buggy程序:
父線程:子線程:int var;//創建子線程 在pthread_create(...) var = 20; var = 10;出口//等孩子 在pthread_join(...) printf(“%d \ n”,var);父線程創建一個小孩。然后兩者都將不同的值寫入一些變量var,然后父級等待孩子退出。
var程序結束時的價值是10或20?我們不知道?該程序被認為是錯誤的(它有競爭),因為最終的值var取決于父和子線程的進度相對速率。如果父母很快,孩子很慢,孩子的任務可能會在以后發生,所以最終的值將是10;?如果孩子比父母快,反之亦然。
父母與孩子的相對進度不是程序員可以控制的,而且經常會從跑步轉為跑步。這取決于諸如機器上的負載,還有什么運行,內核調度策略等許多因素。
明顯的修復是使用鎖來保護var。然而,有意義的是考慮一個更抽象的解決方案,即將消息從一個線程發送到另一個線程:
父線程:子線程:int var;//創建子線程 在pthread_create(...) var = 20; //發送消息給孩子//等待消息到達var = 10;出口//等孩子 在pthread_join(...) printf(“%d \ n”,var);現在程序可靠地打印“10”,不管線程的速度如何。為什么?因為孩子的作業在收到消息后才能發生。并且在父母的分配完成之后,消息不會被發送。
消息傳輸在兩個作業之間創建一個“發生之前”依賴關系:var = 20;?必須在之前發生var = 10;。所以不再有比賽了var。
請注意,父級向孩子發送消息并不重要。從小孩發送消息(在分配后)發送給父母(在分配之前)也會解決問題,導致程序可靠地打印“20”。
Helgrind的算法(在概念上)非常簡單。它監視對內存位置的所有訪問。如果一個位置(在這個例子中?var)被兩個不同的線程訪問,Helgrind會檢查兩次訪問是否被發生在之前的關系中排序。如果是這樣,那沒關系?如果沒有,它報告比賽。
重要的是要明白,發生之前的關系只會產生部分排序,而不是總排序。的總體排序的一個例子是數字的比較:對于任何兩個數字?x和?y,或者?x是小于,等于或大于?y。部分排序就像一個完整的順序,但它也可以表達兩個元素既不相等,更少或更大但僅相對于彼此無序的概念。
在上面的固定例子中,我們說?var = 20;“發生在前”?var = 10;。但在原始版本中,它們是無序的:我們不能說在任何情況下都發生。
說來自不同線程的兩次訪問是通過發生之前的關系來排序的?這意味著有一些線程間同步操作鏈,導致這些訪問以特定順序發生,而不考慮單個線程的實際進度。這是一個可靠的線程程序的必需屬性,這就是Helgrind檢查它的原因。
由標準線程圖元創建的之前的關系如下:
-
當線程T1和稍后(或立即)被線程T2鎖定的互斥鎖解鎖時,在解鎖之前,T1中的存儲器訪問必須在獲取鎖定之后在T2中進行。
-
同樣的想法適用于讀寫器鎖,盡管有一些復雜性,以便允許讀寫對寫入的正確處理。
-
當條件變量(CV)由線程T1發信號通知并且其他線程T2由于相同CV的等待而被釋放時,則信令之前的T1中的存儲器訪問必須發生 - 在T2中的存儲器返回之后這段等待。如果沒有線程等待CV,那么沒有任何效果。
-
如果在CV上廣播T1,則所有等待的線程,而不是僅僅其中一個線程在廣播線路上獲得廣播線程的先天依賴。
-
在線程T1發布的信號量上完成sem_wait之后繼續的線程T2獲取對發布線程的依賴性,有點像依賴關系導致互斥鎖解鎖對。然而,由于信號量可以多次發布,所以在依賴性之前,等待呼叫中哪個發起呼叫發生的時候是未指定的。
-
對于一組線程T1 .. Tn到達屏障然后移動,呼叫之后的每個線程在屏障之前的所有線程發生依賴之后。
-
新創建的子線程在其父級創建點的時候獲取一個初始的依賴關系。也就是說,在創建子進程之前,父進程執行的所有內存訪問都將被視為發生在子進程的所有訪問之前。
-
類似地,當一個退出的線程通過調用獲得pthread_join時,一旦調用返回,收獲線程獲得相對于由退出線程所做的所有存儲器訪問的先后依存關系。
總而言之:Helgrind攔截上述事件,并構建一個有針對性的非循環圖,表示集體發生之前的依賴關系。它還監視所有內存訪問。
如果一個位置被兩個不同的線程訪問,但是Helgrind沒有找到任何路徑,通過發生在之前的圖形從一個訪問到另一個訪問,那么它報告比賽。
有幾個注意事項:
-
在兩次訪問都是讀取的情況下,Helgrind不會檢查一場比賽。這是愚蠢的,因為并發讀取是無害的。
-
即使通過任意長的同步事件鏈,兩個訪問也被認為是由先發生的依賴關系排序的。例如,如果T1訪問某個位置L,然后pthread_cond_signalsT2(后來?pthread_cond_signalsT3接著訪問L),則在第一次和第二次訪問之間存在合適的先后依賴關系,即使它涉及兩個不同的線程間同步事件。
7.4.3。解釋競賽錯誤訊息
Helgrind的種族檢測算法收集了大量信息,并在檢測到比賽時嘗試以有用的方式呈現。以下是一個例子:
線程#2已創建在0x511C08E:克隆(在/lib64/libc-2.8.so)由0x4E333A4:do_clone(在/lib64/libpthread-2.8.so)by 0x4E33A30:pthread_create @@ GLIBC_2.2.5(在/lib64/libpthread-2.8.so中)通過0x4C299D4:pthread_create @ *(hg_intercepts.c:214)通過0x4008F2:main(tc21_pthonce.c:86)線程#3已創建在0x511C08E:克隆(在/lib64/libc-2.8.so)由0x4E333A4:do_clone(在/lib64/libpthread-2.8.so)by 0x4E33A30:pthread_create @@ GLIBC_2.2.5(在/lib64/libpthread-2.8.so中)通過0x4C299D4:pthread_create @ *(hg_intercepts.c:214)通過0x4008F2:main(tc21_pthonce.c:86)線程#3在0x601070讀取大小4的可能數據競爭 鎖定舉行:無在0x40087A:child(tc21_pthonce.c:74)by 0x4C29AFF:mythread_wrapper(hg_intercepts.c:194)通過0x4E3403F:start_thread(在/lib64/libpthread-2.8.so)由0x511C0CC:克隆(在/lib64/libc-2.8.so)這與以前由線程#2寫入的大小4沖突 鎖定舉行:無在0x400883:child(tc21_pthonce.c:74)by 0x4C29AFF:mythread_wrapper(hg_intercepts.c:194)通過0x4E3403F:start_thread(在/lib64/libpthread-2.8.so)由0x511C0CC:克隆(在/lib64/libc-2.8.so)位置0x601070是局部var“unprotected2”內的0字節 在tc21_pthonce.c中聲明:51,在線程3的框架#0中Helgrind首先宣布錯誤消息中引用的任何線程的創建點。這樣就可以簡單地講一下線程,而不會重復打印它們的創建點調用堆棧。每個線程只有一次宣布,首次出現在任何Helgrind錯誤消息中。
主要錯誤信息從文本“?Possible data race during read”?開始。一開始就是您期望看到的信息 - 賽車訪問的地址和大小,無論是讀取還是寫入,以及在檢測到點時的調用堆棧。
第二個調用堆棧從文本“?This conflicts with a previous write”?開始呈現。這顯示了先前的訪問,也訪問了所述的地址,并被認為是與第一個調用堆棧中的訪問競爭。請注意,此第二個調用堆棧限制為最多8個條目以限制內存使用。
最后,Helgrind可能會以源代碼級別的方式來嘗試描述一下比賽地址。在此示例中,它將其標識為局部變量,顯示其名稱,聲明點,以及它居住在哪個第一個調用棧的框架中。請注意,此信息僅--read-var-info=yes?在命令行上指定時顯示。這是因為讀取DWARF3調試信息足夠詳細,以捕獲變量類型和位置信息,使Helgrind在啟動時更慢,并且對于大型程序也需要大量的內存。
一旦你有兩個電話堆棧,你如何找到比賽的根本原因?
首先要檢查每個調用堆棧引用的源位置。它們都應該顯示對相同位置或變量的訪問。
現在弄清楚該位置應該如何線程安全:
-
也許這個位置是由互斥體保護的?如果是這樣,您需要在兩個接入點鎖定和解鎖互斥鎖,即使其中一個訪問被報告為讀取。你有沒有忘記在一個或另外的訪問鎖定?為了幫助您這樣做,Helgrind顯示每個線程在訪問比賽位置時所持有的一組鎖。
-
或者,也許您打算使用其他一些方案來使其安全,例如在條件變量上發信號。在所有這種情況下,嘗試找到一個同步事件(或其一個鏈),它將較早觀察到的訪問(如第二個調用堆棧所示)與稍后觀察到的訪問(如第一個調用堆棧所示)分離。換句話說,嘗試找到證據表明早期訪問“發生在之前”的后期訪問。有關發生之前關系的說明,請參閱上一小節。
Helgrind正在報道一場比賽的事實意味著在兩次訪問之間沒有發生任何關系。如果Helgrind正常工作,那么即使仔細檢查源代碼,您也可能找不到任何此類關系。但是,希望您對代碼的檢查將顯示丟失的同步操作應該在何處。
7.5。有效使用Helgrind的提示和提示
Helgrind可以非常有助于找到和解決與線程相關的問題。像所有復雜的工具一樣,當您了解如何發揮其優勢時,效果最好。
當您僅僅將一個現有的線程程序拋在腦后,Helgrind的效率將會降低,并且嘗試了解任何報告的錯誤。如果您從一開始就設計線程程序,以幫助Helgrind驗證正確性,那將會更有效。使用Memcheck查找內存錯誤也是如此,但在這里應用更多,因為線程檢查是一個更難的問題。因此,編寫一個正確的程序要比Helcred錯誤地報告(線程)錯誤要寫正確的程序更容易,Memcheck錯誤地報告(內存)錯誤。
考慮到這一點,這里有一些提示,最重要的是列出最可靠的結果并避免錯誤的錯誤。前兩個是至關重要的。任何違反他們的行為都將使您大量虛假的數據競賽錯誤。
確保您的應用程序及其使用的所有庫都使用POSIX線程原語。Helgrind需要能夠看到與線程創建,退出,鎖定和其他同步事件有關的所有事件。為此,它攔截了許多POSIX pthreads函數。
不要將自己的線程原語(mutexes等)從Linux futex系統調用,原子計數器等的組合中滾出來。這些引發了Helgrind內部的不斷變化的模式,并會給出虛假的結果。
另外,不要使用其他POSIX抽象來重新實現現有的POSIX抽象。例如,不要從POSIX互斥體和條件變量構建自己的信號量例程或讀寫器鎖。因為Helgrind直接支持它們,而是直接使用POSIX讀寫器鎖和信號量。
Helgrind直接支持以下POSIX線程抽象:互斥體,讀寫器鎖,條件變量(見下文),信號量和障礙。目前,螺旋鎖不支持,盡管它們可能在將來。
在撰寫本文時,以下流行的Linux軟件包已知可以實現自己的線程圖元:
-
Qt版本4.X.?Qt 3.X是無害的,因為它只使用POSIX pthread的原語。不幸的是,Qt 4.X有自己的互斥體(QMutex)和線程捕獲的實現。Helgrind 3.4.x包含對Qt 4.X線程的直接支持,這是實驗性的,但被認為工作得很好。直接支持Qt 4的副作用是Helgrind可用于調試KDE4應用程序。由于這是一個實驗性的功能,我們特別希望有人使用Helgrind來成功調試Qt 4和/或KDE4應用程序的反饋。
-
GNU OpenMP(GCC的一部分)的運行時支持庫,至少對于GCC版本4.2和4.3。GNU OpenMP運行時庫(libgomp.so)使用原子存儲器指令和futex系統調用的組合構建自己的同步原語,這導致了Helgrind之后的混亂,因為它不能“看到”這些。
幸運的是,這可以使用配置時選項(GCC)來解決。從源代碼重建GCC,并配置使用?--disable-linux-futex。這使得libgomp.so代替使用標準的POSIX線程原語。請注意,這是使用GCC 4.2.3進行測試,并沒有使用更近的GCC版本進行重新測試。我們很高興聽到有關更新版本的任何成功或失敗。
如果您必須實現自己的線程圖元,那么有一組客戶端請求宏helgrind.h來幫助您將其原始圖形描述為Helgrind。您應該可以毫不費力地標記互斥體,條件變量等。
也可以使用和ANNOTATE_HAPPENS_BEFORE,?ANNOTATE_HAPPENS_AFTER和?ANNOTATE_HAPPENS_BEFORE_FORGET_ALL宏來標記線程安全引用計數的影響?。使用原子遞增/遞減的引用計數變量的線程安全引用計數導致Helgrind問題,因為引用計數的一到零轉換意味著訪問線程具有關聯資源(通常為C ++對象)的獨占所有權,因此可以訪問它(通常是運行它的析構函數)而不鎖定。Helgrind不明白這一點,加標是避免誤報的關鍵。
以下是C ++中線程安全引用計數的建議指南。您只需要標記您的發布方法 - 減少引用計數的方法。給了這樣一個類:
MyClass類{unsigned int mRefCount;void Release(void){unsigned int newCount = atomic_decrement(&mRefCount);if(newCount == 0){刪除這個;}} }釋放方法應標注如下:
void Release(void){unsigned int newCount = atomic_decrement(&mRefCount);if(newCount == 0){ANNOTATE_HAPPENS_AFTER(&mRefCount);ANNOTATE_HAPPENS_BEFORE_FORGET_ALL(&mRefCount);刪除這個;} else {ANNOTATE_HAPPENS_BEFORE(&mRefCount);}}這個方案有一些復雜的,大多數是理論上的反對意見。從理論的角度來看,似乎不可能制定一個完全正確的標記方案,即保證消除所有的虛假種族的意義。所提出的方案在實踐中表現良好。
避免記憶回收。如果你不能避免它,你必須使用告訴Helgrind通過?VALGRIND_HG_CLEAN_MEMORY客戶端請求(in?helgrind.h)發生了什么。
Helgrind知道通過malloc/?free/?new/?delete?和堆棧幀的進入和退出發生的標準堆內存分配和釋放?。特別是當記憶通過free,delete或功能退出釋放時,Helgrind認為記憶清潔,所以當它最終被重新分配時,它的歷史是無關緊要的。
然而,通常的做法是實現內存回收方案。在這些中,要釋放的內存不會傳遞給?free/?delete,而是放入一個可用緩沖區的池中,以便根據需要再次發出。問題是Helgrind無法知道這樣的記憶在邏輯上不再被使用,它的歷史是無關緊要的。因此,您必須使用明確的方式,使用VALGRIND_HG_CLEAN_MEMORY客戶端請求來指定相關的地址范圍。將這些請求放入池管理器代碼是最簡單的方法,當內存返回到池中或從中分配時使用它們。
避免POSIX條件變量。如果可以,使用POSIX信號量(sem_t,sem_post,?sem_wait)做線程間的事件信號。初始值為零的信號量對此尤其有用。
Helgrind僅部分正確處理POSIX條件變量。這是因為Helgrind?只有在等待的線程首先到達會合(才能實際調用)的時候才能看到pthread_cond_wait調用和?pthread_cond_signal/ /?pthread_cond_broadcast調用?之間的線程間依賴關系pthread_cond_wait。如果信號器首先到達,則不能看到線程之間的依賴關系。在后一種情況下,POSIX指南意味著相關聯的布爾狀態仍然提供線程間同步事件,但Helgrind不可見的同步事件。
Helgrind缺少一些線程間同步事件的結果是使其報告誤報。
這種同步損失的根本原因尤其難以理解,所以一個例子是有幫助的。已經由Arndt Muehlenfeld(“多線程程序運行時候賽檢測”,論文,奧地利TU格拉茨)進行了討論。條件變量的規范POSIX推薦使用方案如下:
b是布爾條件,大部分時間是False cv是條件變量 mx是其相關的互斥體信使者:服務員:鎖(mx)鎖(mx) b = True while(b == False) 信號(cv)等待(cv,mx) 解鎖(mx)解鎖(mx)假設b大部分時間是假的。如果服務員首先到達會合點,則進入其同步循環,等待信號器發出信號,并最終繼續進行。Helgrind看到信號,注意到依賴,一切都很好。
如果b信號器首先到達,則設置為true,并且信號消失在無處。當服務員稍后到達時,它不進入其循環,只是繼續進行。但即使在這種情況下,跟蹤while循環后的服務器代碼也不能執行,直到信號器設置b為True。因此,仍然有相同的線程間依賴性,但是這次通過任意的內存條件,Helgrind看不到它。
相比之下,Helgrind對由信號量操作引起的線程間依賴性的檢測被認為是完全正確的。
據我所知,這個問題的解決方案不需要條件變量等待循環的源代碼級注解超出了現有技術。
確保您正在使用受支持的Linux發行版。目前,Helgrind只能正確支持glibc-2.3或更高版本。這反過來意味著我們只支持glibc的NPTL線程實現。舊的LinuxThreads實現不受支持。
如果您的應用程序使用線程局部變量,helgrind可能會報告這些變量的錯誤的正面競爭條件,盡管很可能是免費的。在Linux上,您可以使用--sim-hints=deactivate-pthread-stack-cache-via-hack?以避免這種假陽性錯誤消息(請參閱--sim-hints)。
使用完成所有線程?pthread_join。避免分離線程:不要在分離狀態下創建線程,并且不要調用pthread_detach現有線程。
使用pthread_join完成的線程可以提供Helgrind和程序員可以看到的清晰的同步點。如果您不調用?pthread_join線程,Helgrind相對于程序中其他線程的任何重要同步點,無法知道何時完成。因此,它假定線程無限期地停留,并且可能無限期地與程序的存儲器狀態干涉。它有權假設 - 畢竟,由于安排原因,退出的線程在其生命的最后階段確實運行得非常緩慢。
一起執行線程調試(使用Helgrind)和內存調試(使用Memcheck)。
Helgrind詳細跟蹤內存狀態,應用程序中的內存管理錯誤可能會導致混亂。在極端情況下,已知有許多無效讀取和寫入(特別是釋放內存)的應用程序會使Helgrind崩潰。因此,理想情況下,您應該在使用Helgrind之前使您的應用程序Memcheck-clean。
除非您首先刪除線程錯誤,否則可能無法使您的應用程序Memcheck-clean。特別地,在程序終止時,可能難以刪除在多線程C ++析構函數序列中釋放內存的所有讀寫操作。所以,理想情況下,在使用Memcheck之前,應該使您的應用程序Helgrind-clean。
由于這個圓形顯然是無法解決的,至少要記住,Memcheck和Helgrind在一定程度上是相輔相成的,你可能需要一起使用它們。
POSIX要求的標準I / O(的實現printf,fprintf,?fwrite,fread,等)是線程安全的。不幸的是,GNU libc通過使用Helgrind無法攔截的內部鎖定原語來實現。因此,當您使用這些功能時,Helgrind會產生許多虛假的競賽報告。
Helgrind嘗試使用標準的Valgrind錯誤抑制機制來隱藏這些錯誤。所以,至少對于簡單的測試用例,你看不到任何的。然而,有些可能會滑過。只是一些要注意的事情。
Helgrind的錯誤檢查在系統線程庫本身(libpthread.so)中無法正常工作,并且通常會在其中觀察到大量(false)錯誤。Valgrind的抑制系統然后過濾掉這些,所以你不應該看到它們。
如果你看到報道任何地方比賽失誤libpthread.so或者?ld.so是最里面的堆棧幀關聯的對象,請在提交錯誤報告?http://www.valgrind.org/。
7.6。Helgrind命令行選項
以下最終用戶選項可用:
--free-is-write=no|yes [default: no]啟用時(不是默認值),Helgrind處理釋放堆內存,就好像內存是在空閑之前寫的。這暴露了一個線程引用內存的種族,并被另一個線程釋放,但沒有可觀察到的同步事件,以確保引用在免費之前發生。
這個功能是Valgrind 3.7.0中的新功能,被認為是實驗性的。默認情況下不啟用它,因為它與自定義內存分配器的交互目前還不太了解。歡迎用戶反饋。
啟用(默認)時,Helgrind執行鎖定順序一致性檢查。對于一些錯誤的程序,報告的大量鎖定順序錯誤可能會變得煩人,特別是如果您只對種族錯誤感興趣。因此,您可能會發現禁用鎖定順序檢查有用。
--history-level=full(默認)使Helgrind收集有關“舊”訪問的足夠信息,它可以在競爭報告中生成兩個堆棧跟蹤 - 當前訪問的堆棧跟蹤以及較舊的沖突訪問的跟蹤。為了限制內存使用,“舊”訪問堆棧跟蹤最多限制為8個條目,即使?--num-callers值較大。
收集這些信息在速度和存儲器中都是昂貴的,特別是對于執行許多線程間同步事件(鎖定,解鎖等)的程序而言。沒有這樣的信息,更難以追查種族的根本原因。盡管如此,您可能不需要它,只需要檢查是否存在種族,例如在進行以前無競爭程序的回歸測試時。
--history-level=none是相反的極端。它使Helgrind不會收集有關以前訪問的任何信息。這可以快得多--history-level=full。
--history-level=approx在這兩個極端之間提供了妥協。它會導致Helgrind顯示后續訪問的完整跟蹤信息,并且大概有關早期訪問的信息。這個近似信息由兩個堆棧組成,較早的訪問保證發生在由兩個堆棧表示的程序點之間的某處。這不如顯示以前訪問的確切堆棧(同樣--history-level=full),但它比沒有更好,它幾乎一樣快?--history-level=none。
這個標志只能有效果--history-level=full。
關于“舊”沖突訪問的信息存儲在具有LRU風格管理的有限大小的高速緩存中。這是必要的,因為存儲由程序進行的每個單個存儲器訪問的堆棧跟蹤是不實際的。定期丟棄不近期訪問的位置的歷史信息,以釋放高速緩存中的空間。
該選項根據存儲有沖突的訪問信息的不同內存地址的數量來控制緩存的大小。如果您發現Helgrind僅使用一個堆棧而不是預期的兩個堆棧顯示種族錯誤,請嘗試增加此值。
最小值為10,000,最大值為30,000,000(默認值的三十倍)。將值增加1將Helgrind的內存需求提高大約100個字節,因此最大值將容易地占用三個額外的千兆字節內存。
默認情況下,Helgrind會檢查您的程序進行的所有數據存儲器訪問。該標志允許您跳過檢查對線程堆棧(局部變量)的訪問。這可以提高性能,但是以堆棧分配的數據丟失競賽為代價。
控制線程創建過程中是否應忽略所有活動。默認情況下僅在Solaris上啟用。Solaris提供了比其他操作系統更高的吞吐量,并行性和可擴展性,代價是更細粒度的鎖定活動。這意味著例如,當在glibc下創建一個線程時,所有線程設置只使用一個大鎖。Solaris libc使用幾個細粒度的鎖,并且創建者線程盡快恢復其活動,例如堆棧和TLS設置順序到創建的線程。這種情況使Helgrind感到困惑,因為它假定在創建者和創建的線程之間存在一些錯誤的順序;?因此,不會報告申請中的許多類型的種族條件。yes為了防止這種錯誤排序, 在Solaris上默認情況下將此命令行選項設置為。所有活動(加載,存儲,客戶端請求)因此在以下期間被忽略:
-
pthread_create()在創建者線程中調用
-
線程創建階段(堆棧和TLS設置)在創建的線程中
在線程創建期間分配的新內存也將被追溯,那就是競賽報告被抑制。DRD隱含地做同樣的事情。這是必要的,因為Solaris libc緩存了許多對象,并為不同的線程重用它們,并且使Helgrind混淆。
7.7。Helgrind監視器命令
Helgrind工具提供由Valgrind內置的gdbserver?處理的監視器命令(請參閱Valgrind gdbserver的Monitor命令處理)。
-
info locks [lock_addr]顯示鎖的列表及其狀態。如果?lock_addr給出,只顯示位于該地址的鎖。
在以下示例中,helgrind知道一個鎖。此鎖位于訪客地址ga 0x8049a20。鎖類型表示rdwr?讀寫器鎖。其他可能的鎖類型是nonRec(簡單互斥體,非遞歸)和mbRec(簡單互斥體,可能是遞歸的)。然后鎖類型后面是保存鎖的線程列表。在下面的示例中,R1:thread #6 tid 3?表示helgrind線程#6已經獲取(一旦作為字母R之后的計數器為1)鎖定在讀取模式。helgrind線程nr為每個啟動的線程遞增。“tid 3”的存在表示線程#6還沒有退出,并且是valgrind tid 3.如果一個線程已經終止,那么用“tid(退出)”指示。
(gdb)監視器信息鎖 鎖ga 0x8049a20 {親切的rdwr{R1:thread#6 tid 3} } (GDB)如果您提供該選項--read-var-info=yes,則將提供有關鎖定位置的更多信息,例如包含該鎖的全局變量或堆塊:
鎖ga 0x8049a20 {位置0x8049a20是全局var“s_rwlock”內的0個字節在rwlock_race.c中聲明:17親切的rdwr{R1:thread#3 tid 3} } -
accesshistory <addr> [<len>]?顯示從<addr>開始的<len>(默認為1)字節記錄的訪問歷史記錄。對于與給定范圍重疊的每個記錄的訪問,accesshistory顯示操作類型(讀取或寫入),讀取或寫入的地址和大小,執行操作的helgrind線程nr / valgrind tid號碼以及由線程持有的鎖定操作時間。首先顯示最早的訪問權限,最近的訪問權限顯示在最后。
在下面的例子中,我們首先看到已經修改了給定的2個字節范圍的線程#7記錄了4個字節的寫入。第二個記錄的寫入是最近記錄的寫入:線程#9修改了相同的2個字節作為4字節寫入操作的一部分。還會顯示每個線程在寫操作時保持的鎖的列表。
(gdb)monitor accesshistory 0x8049D8A 2 通過線程#7寫入大小為4的0x8049D88 tid 3 == 6319 ==鎖定:2,地址0x8049D8C(和1,無法顯示) == 6319 == at 0x804865F:child_fn1(locked_vs_unlocked2.c:29) == 6319 == 0x400AE61:mythread_wrapper(hg_intercepts.c:234) == 6319 == by 0x39B924:start_thread(pthread_create.c:297) == 6319 == by 0x2F107D:clone(clone.S:130)通過線程#9寫入大小為4的0x8049D88 tid 2 == 6319 ==鎖定:2,地址0x8049DA4 0x8049DD4 == 6319 == at 0x804877B:child_fn2(locked_vs_unlocked2.c:45) == 6319 == 0x400AE61:mythread_wrapper(hg_intercepts.c:234) == 6319 == by 0x39B924:start_thread(pthread_create.c:297) == 6319 == by 0x2F107D:clone(clone.S:130)
7.8。Helgrind客戶端請求
下面定義了以下客戶端請求?helgrind.h。看到這個文件的參數的確切細節。
-
VALGRIND_HG_CLEAN_MEMORY
這使得Helgrind忘記了關于指定內存范圍的所有內容。這對于希望回收內存的內存分配器特別有用。
-
ANNOTATE_HAPPENS_BEFORE
-
ANNOTATE_HAPPENS_AFTER
-
ANNOTATE_NEW_MEMORY
-
ANNOTATE_RWLOCK_CREATE
-
ANNOTATE_RWLOCK_DESTROY
-
ANNOTATE_RWLOCK_ACQUIRED
-
ANNOTATE_RWLOCK_RELEASED
這些用于向Helgrind描述自定義(非POSIX)同步原語的行為,否則無法理解。請參閱helgrind.h進一步文檔的注釋。
7.9。Helgrind的待辦事項清單
以下是一些松散的結束列表,應該整理一下。
-
對于鎖定順序錯誤,打印完整的鎖定循環,而不是僅在目前的2個周期內執行。
-
有沖突的訪問機制有時神秘地不能顯示沖突的訪問“堆棧,即使提供有無沖突的訪問信息的無界存儲。這應該進行調查。
-
由GCC為投機商店創建的線程不安全代碼引起的文檔競賽。在臨時看?http://gcc.gnu.org/ml/gcc/2007-10/msg00266.html?和http://lkml.org/lkml/2007/10/24/673。
-
不要更新鎖定順序圖,并且不要檢查是否出現“嘗試”狀態的鎖定操作時的錯誤(例如?pthread_mutex_trylock)。這樣的呼叫不會對鎖定順序添加任何實際的限制,因為它們總是無法獲取鎖定,導致呼叫者離開并執行計劃B(大概它將有一個計劃B)。進行這樣的檢查可能會產生錯誤的鎖定順序錯誤并混淆用戶。
-
表現可能非常差?100:1的減速并不罕見。性能改進的范圍有限。
總結
以上是生活随笔為你收集整理的Helgrind:螺纹错误检测器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 对于机器学习中,数据增强
- 下一篇: USACO-Section1.4 Wor