我有一個關于條件變數和相關互斥鎖之間相互作用的問題(它源于我在講座中提出的一個簡化示例,在這個程序中讓自己感到困惑)。兩個執行緒通過行程記憶體中的共享變數交換資料(比如說一個int n指示陣列大小,一個double *d指向陣列)。我使用附加int flag(最初flag = 0)來指示資料(n和d)何時準備好(flag = 1)、apthread_mutex_t mtx和條件變數pthread_cond_t cnd。
這部分來自接收執行緒,它等待直到flag成為1互斥鎖的保護下,但隨后處理n并且d沒有保護:
while (1) {
pthread_mutex_lock(&mtx);
while (!flag) {
pthread_cond_wait(&cnd, &mtx);
}
pthread_mutex_unlock(&mtx);
// use n and d
}
這部分來自發送者執行緒,它在沒有互斥鎖保護的情況下預先設定,但n在互斥鎖被鎖定時設定:dflag
n = 10;
d = malloc(n * sizeof(float));
pthread_mutex_lock(&mtx);
flag = 1;
pthread_cond_signal(&cnd);
pthread_mutex_unlock(&mtx);
很明顯,您需要發送方中的互斥鎖,否則您會遇到“丟失的喚醒呼叫”問題(請參閱https://stackoverflow.com/a/4544494/3852630)。
我的問題有所不同:我不確定必須在互斥鎖保護的區域內設定(在發送者執行緒中)或讀出(在接收者執行緒中)哪些變數,以及哪些變數不需要保護通過互斥鎖。flag雙方都保護就足夠了,還是需要保護n也d需要保護?
發送方和接收方之間的記憶體可見性(參見下面的規則)應該通過呼叫來保證pthread_cond_signal(),因此應該存在必要的成對記憶體屏障(與 結合使用pthread_cond_wait())。
我知道這是一個不尋常的案例。通常,我的應用程式會修改發送方的任務串列并從接收方的串列中彈出任務,并且關聯的互斥鎖會保護雙方的串列操作。但是,我不確定在上述情況下需要什么。危險是否在于編譯器(它不知道對變數的并發訪問)優化了對變數的寫入和/或讀取訪問?n如果并且d不受互斥鎖保護,是否還有其他問題?
謝謝你的幫助!
David R. Butenhof:使用 POSIX 執行緒編程,第 89 頁:“執行緒可以看到的任何記憶體值何時是信號……條件變數也可以被該信號喚醒的任何執行緒看到……”。
uj5u.com熱心網友回復:
在記憶體排序的底層,規則不是根據“受互斥體保護的區域”來指定的,而是根據同步來指定的。每當兩個不同的執行緒訪問同一個非原子物件時,除非它們都是讀取,否則它們之間必須有一個同步操作,以確保其中一個訪問肯定發生在另一個之前。
實作同步的方法是讓一個執行緒在訪問共享變數后執行釋放操作(例如解鎖互斥鎖),并在訪問共享變數之前讓另一個執行緒執行獲取操作(例如鎖定互斥鎖)。程式邏輯保證獲取必須在發布之后發生的方式。
在這里,你有那個。n發送者執行緒在訪問和d(發送者代碼的最后一行)后確實執行了互斥鎖解鎖。并且接收者在訪問它們之前會執行互斥鎖(內部pthread_cond_wait)。的設定和測驗flag確保當我們退出while (!flag)回圈時,接收方最近對互斥鎖的鎖定確實發生在發送方解鎖之后。這樣就實作了同步。
編譯器和 CPU 不得執行任何會破壞這一點的優化,因此特別是它們不能優化對 and 的訪問n,d或圍繞同步操作重新排序它們。這通常通過將釋放/獲取操作視為障礙來確保。在釋放屏障之前以程式順序發生的對潛在共享物件的任何訪問都必須在屏障之后的任何操作之前實際執行并重繪 到一致的記憶體/快取(在這種情況下,在任何其他執行緒可能將互斥鎖視為解鎖之前) . 如果需要特殊的 CPU 指令來確保全域可見性,編譯器必須發出它們。對于獲取屏障反之亦然:在獲取屏障之后按程式順序發生的任何訪問都不得在它之前重新排序。
換句話說,編譯器將釋放屏障視為可能讀取所有記憶體的操作。所以所有變數都必須在該點之前寫出,以便該點的實際記憶體內容與抽象機器的內容相匹配。同樣,獲取屏障被視為可能寫入所有記憶體的操作,并且之后必須從記憶體中重新加載所有變數。唯一的例外是區域變數,編譯器可以證明沒有其他執行緒可以合法地知道它們的地址。這些可以安全地保存在暫存器中或以其他方式重新排序。
確實,在同步鎖定操作之后,接收方恰好再次解鎖了互斥鎖,但這與這里無關。該解鎖不會與此特定程式中的任何內容同步,并且不會影響其執行。同樣,為了同步訪問nand d,發送者在訪問互斥鎖之前還是之后鎖定互斥鎖并不重要。(盡管發送者在寫入之前鎖定互斥鎖很重要flag;這就是我們如何確保flag接收者之前的任何讀取確實發生在寫入之前,而不是與它競爭。)
“對共享變數的訪問應該在受互斥鎖保護的關鍵區域內”的原則只是一種更高級別的抽象,它是確保不同執行緒的訪問始終在它們之間具有同步解鎖鎖定對的一種方法。并且在變數可以被反復訪問的情況下,您通常需要在每次訪問之前鎖定和之后解鎖,這相當于“臨界區”原則。但這個原則本身并不是基本規則。
也就是說,在現實生活中,您可能確實希望盡可能地遵循這一原則,因為這樣可以更輕松地撰寫正確的代碼并避免細微的錯誤,同時也使其他程式員更容易驗證您的代碼是否正確。因此,即使在此程式中訪問n,d被互斥體“保護”并不是絕對必要的,但無論如何這樣做可能是明智的,除非獲得顯著且可衡量的好處(性能或其他方面)通過避免它。
條件變數在此處的競爭避免中不起作用,除非pthread_cond_wait鎖定互斥鎖。從功能上講,它相當于讓接收器簡單地執行“鎖定互斥體;測驗標志;解鎖互斥體”的緊密自旋回圈,但不會浪費所有這些 CPU 周期。
而且我認為布滕霍夫的引述是錯誤的,或者充其量是誤導性的。我的理解是,pthread_cond_signal它本身并不能保證成為任何型別的障礙,實際上沒有任何記憶排序效應。POSIX 不直接解決記憶體排序問題,但標準 C 等效項就是這種情況cnd_signal。pthread_cond_signal除非您可以通過假設所有這些訪問在相應pthread_cond_wait回傳時可見來使用它,否則確保全域可見性是沒有意義的。但既然pthread_cond_wait可以虛假地醒來,你永遠不能安全地做出這樣的假設。
在這個程式中,sender 中必要的釋放屏障不是來自pthread_cond_signal,而是來自后續的pthread_mutex_unlock。
uj5u.com熱心網友回復:
兩邊保護flag就夠了,還是n和d也需要保護?
原則上,如果你使用你的互斥鎖,CV,并且在設定互斥鎖保護后,flag作者不修改n,,d或設定后,讀者無法訪問,,或直到它觀察到修改后(受保護相同的互斥體),那么您可以依靠閱讀器觀察作者最后寫入的、和值。這或多或少是一個手卷信號量。*dflagnd*dflagnd*d
在實踐中,您應該使用您選擇的任何系統同步物件(互斥鎖、信號量等)來保護所有共享資料。這樣做更容易推理并且不太容易產生錯誤。通常,它也更簡單。
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/496648.html
下一篇:當將文本檔案中的串列讀取到pandas資料框列時,面臨`ParserError:標記資料時出錯。C錯誤:預計第3行中有16個欄位,看到21`
