2011-11-28 62 views
24

我正在使用全局變量實現線程間通信。是賦值運算符'='原子?

//global var 
volatile bool is_true = true; 

//thread 1 
void thread_1() 
{ 
    while(1){ 
     int rint = rand() % 10; 
     if(is_true) { 
      cout << "thread_1: "<< rint <<endl; //thread_1 prints some stuff 
      if(rint == 3) 
       is_true = false; //here, tells thread_2 to start printing stuff 
     } 
    } 
} 

//thread 2 
void thread_2() 
{ 
    while(1){ 
     int rint = rand() % 10; 
     if(! is_true) { //if is_true == false 
      cout << "thread_1: "<< rint <<endl; //thread_2 prints some stuff 
      if(rint == 7) //7 
       is_true = true; //here, tells thread_1 to start printing stuff 
     } 
    } 
} 

int main() 
{ 
    HANDLE t1 = CreateThread(0,0, thread_1, 0,0,0); 
    HANDLE t2 = CreateThread(0,0, thread_2, 0,0,0); 
    Sleep(9999999); 
    return 0; 
} 

問題

在上面的代碼中,我使用全局變量volatile bool is_true切換thread_1和thread_2之間打印。

我不知道這裏是否使用賦值操作是線程安全的

+0

我寧願使用原子交換原語,但我不能解決你會遇到問題的場景...... –

+0

@KerrekSB,這個場景?那麼,我只是簡單地展示了我的問題,:) – Alcott

+0

嗯,我的意思是一系列的加載和存儲將被充分打破,使兩個線程進入關鍵部分...通常應該能夠展示這樣一個序列以說明爲什麼某些代碼不正確。雖然我在這裏看不到它。我仍然不喜歡代碼,但我無法證明爲什麼。 –

回答

54

此代碼不保證在Win32上是線程安全的,因爲Win32只保證正確對齊的4字節和指針大小的值的原子性。 bool不保證是其中的一種。 (它通常是1個字節的類型。)

對於那些誰需要這如何可能會失敗一個實際的例子:

假設bool是1個字節的類型。假設您的is_true變量正好存儲在另一個bool變量(我們稱之爲other_bool)附近,以便它們共享相同的4字節行。具體來說,假設is_true位於地址0x1000,而other_bool位於地址0x1001。假設兩個值最初都是false,並且一個線程決定在另一個線程嘗試更新other_bool的同時更新is_true。可以發生的操作的順序如下:

  • 線程1準備由裝載包含is_trueother_bool 4字節的值,以設置is_truetrue。線程1讀取0x00000000。
  • 線程2準備通過加載包含is_trueother_bool的4字節值來設置other_booltrue。線程2讀取0x00000000。
  • 線程1更新對應於is_true的4字節值中的字節,產生0x00000001。
  • 線程2更新對應於other_bool的4字節值中的字節,產生0x00000100。
  • 線程1將更新值存儲到內存。 is_true現在是trueother_bool現在是false
  • 線程2將更新值存儲到內存。 is_true現在是falseother_bool現在是true

可觀察到結束該序列中,更新is_true丟失了,因爲它是由螺紋2,其拍攝的is_true一箇舊值覆蓋。

x86發生這種類型的錯誤時非常容易,因爲它支持字節粒度更新並具有非常緊密的內存模型。其他Win32處理器並不如此寬容。例如,RISC芯片通常不支持字節粒度更新,即使他們這樣做,他們通常也有非常弱的內存模型。

+0

關於對齊的很好的解釋你的例子清楚地表明賦值不是原子的 –

+3

順便說一句,'volatile'關鍵字不會強制編譯器以線程安全的方式將變量與目標體系結構對齊(即存儲對於32位x86處理器,4字節單元格中的「bool」值)?這可以解決問題。一些編譯器是否會這樣做? –

+1

@PavelGatilov根據最新的草案標準,編譯器必須阻止這種行爲 – curiousguy

7

不,它不是.....你需要使用某種鎖定原語。根據平臺,你可以使用boost,或者使用本地窗口,比如InterlockedCompareExchange。

事實上,在您的情況下,您可能需要使用某些線程安全的事件機制,以便您可以「通知」其他線程開始按照您的要求進行操作。

+2

雖然OP的代碼有什麼錯誤?你可以設計一個場景中斷的地方嗎? –

+0

@KerrekSB,對不起,OP?那是什麼? – Alcott

+2

@Alcott:「OP」的意思是「原始海報」,如在提問的人中。在這種情況下,就是你。 :-) – ruakh

-1

這段代碼的線程安全性不依賴於賦值的原子性。兩個線程都依次嚴格運行。沒有競爭條件:thread_1會輸出一些東西,直到獲得一定的隨機數,然後它將離開'輸出部分'並讓其他線程在其中工作。 有一個值得注意的幾件事情,但:

  • rand()函數可能不是線程安全的(而不是在代碼這裏給出雖然問題)
  • 你不應該使用Win32函數的CreateThread()特別是當您使用CRT潛在函數(潛在地)利用全局變量時。使用_beginthreadex()來代替。
+2

」_兩個程序都嚴格依次工作。 ? – curiousguy

4

在所有現代處理器上,您都可以假設讀取和寫入自然對齊的本機類型是原子的。只要內存總線至少與正在讀取或寫入的類型一樣寬,CPU就會在單個總線事務中讀取和寫入這些類型,從而使其他線程無法在半完成狀態下看到它們。在那裏的x86和x64上,不能保證讀取和寫入比原始大小的八個字節大大的。這意味着流式SIMD擴展(SSE)寄存器和字符串操作的16字節讀寫操作可能不是原子操作。

讀取和寫入不自然對齊的類型(例如,寫入跨越四字節邊界的DWORD)並不保證是原子性的。 CPU可能必須將這些讀寫操作做爲多總線事務,這可能會允許另一個線程在讀取或寫入過程中修改或查看數據。