2009-01-15 81 views
1

以下構造是線程安全的,假設foo的元素已對齊並且大小正確,從而不會出現字詞撕裂?如果不是,爲什麼不呢?將變量同時更新爲同一值的線程安全

注意:下面的代碼是我想要做的玩具例子,而不是我真實的現實情景。顯然,在我的例子中,有更好的方式來編碼可觀察行爲。

uint[] foo; 
// Fill foo with data. 

// In thread one: 
for(uint i = 0; i < foo.length; i++) { 
    if(foo[i] < SOME_NUMBER) { 
     foo[i] = MAGIC_VAL; 
    } 
} 

// In thread two: 
for(uint i = 0; i < foo.length; i++) { 
    if(foo[i] < SOME_OTHER_NUMBER) { 
     foo[i] = MAGIC_VAL; 
    } 
} 

這顯然乍看上去不安全,所以我還是要強調,爲什麼我認爲這可能是安全的:

  1. 只有兩個選項是FOO的元素將保持不變或將設置爲MAGIC_VAL。
  2. 如果線程2在更新中看到foo [i]處於中間狀態,則只會發生兩件事:中間狀態爲< SOME_OTHER_NUMBER或不是。如果它是< SOME_OTHER_NUMBER,則線程2也會嘗試將其設置爲MAGIC_VAL。如果不是,線程2將不會執行任何操作。

編輯:另外,如果foo是一個長或雙或什麼,以便更新它不能做原子?你可能仍然認爲對齊等是更新foo的一個元素不會影響任何其他元素的。此外,在這種情況下,多線程的全部重點都是性能,所以任何類型的鎖定都會打敗它。

回答

0

如果讀取和寫入每個數組元素都是原子的(即它們正確對齊,並且沒有像前面提到的那樣撕裂文字),那麼在這段代碼中應該沒有任何問題。如果foo[i]小於SOME_NUMBERSOME_OTHER_NUMBER中的任一個,則至少一個線程(可能兩者)將在某個點將其設置爲MAGIC_VAL;否則,它將不受影響。隨着原子讀寫,沒有其他的可能性。

但是,由於您的情況更加複雜,請非常小心 - 確保foo[i]真正只在每個循環中讀取一次並存儲在局部變量中。如果您在同一次迭代中多次閱讀它,則可能會得到不一致的結果。即使您對代碼進行的一點點修改都可能會立即使其對於競爭條件不安全,因此大量評論帶有大紅色警告標誌的代碼。

0

這是不好的做法,你不應該處於兩個線程同時訪問同一個變量的狀態,而不管後果如何。你給的例子是過於簡化,任何大多數複雜的樣本幾乎總會有與之相關的問題.. ...

記住:信號量是你的朋友!

0

該特定示例是線程安全的。

這裏沒有真正涉及的中間狀態。 該特定的程序不會感到困惑。儘管如此,我建議在陣列上使用互斥體

1

您可以通過比較和交換操作安全且無鎖地執行此操作。你有什麼看起來線程安全,但編譯器可能會在某些情況下創建一個未更改的值的寫回,這將導致一個線程在另一個線程上。

此外,你可能沒有獲得儘可能多的性能,因爲這樣做會導致CPU緩存內部的MESI transitions風暴,每個線程都是相當的慢。有關多線程內存一致性的更多詳細信息,請參閱Ulrich Drepper的「​​」的第3.3.4節。

+0

是的,對於這樣一個簡單的for循環來說,在兩個線程中執行它可能比僅僅執行一個循環慢,因爲它們共享數據並且(尤其是不好的)寫入它。負面可擴展性的萬歲! – 2009-01-15 22:38:13

4

在現代多核處理器上,您的代碼不是線程安全的(至少在大多數語言中)沒有內存障礙。簡而言之,沒有明確的障礙,每個線程都可以從緩存中看到foo的完全不同的副本。

假設你的兩個線程在某個時間點運行,那麼在稍後的某個時間點,第三個線程會讀取foo,它可能會看到一個完全未初始化的foo或其他兩個線程中的任何一個的foo,或者兩者兼有,這取決於CPU內存緩存發生了什麼。

我的建議 - 不要試圖對併發性「聰明」,總是儘量保持「安全」。聰明會每一次咬你。 broken double-checked locking文章有一些令人大開眼界的見解,說明在沒有內存障礙的情況下(儘管專門針對Java和它(改變)的內存模型,它對任何語言都很有見地),內存訪問和指令重新排序會發生什麼。

你必須真正在你的語言的指定內存模型之上進行快捷屏障。例如,Java允許變量被標記爲volatile,與一個被記錄爲具有原子分配的類型結合,可以允許非同步分配並通過將其強制轉移到主存儲器來獲取(所以線程沒有觀察/更新緩存副本) 。