2008-09-16 43 views
27

在C編程語言和Pthreads中作爲線程庫;線程之間共享的變量/結構是否需要聲明爲volatile?假設他們可能受到鎖的保護(也許是障礙)。使用C/Pthreads:共享變量需要變化嗎?

pthread POSIX標準對此有什麼意見,這是編譯器依賴還是兩者都不?

編輯即可添加:感謝您的出色答案。但如果你是而不是使用鎖;例如,如果您使用barriers?或者使用諸如compare-and-swap之類的原語來直接並原子地修改共享變量的代碼...

回答

5

我認爲volatile的一個非常重要的特性是,它使變量在修改時寫入內存,並在每次訪問時從內存重新讀取。這裏的其他答案混合了易失性和同步性,並且從其他一些答案中可以清楚地看出,易失性不是同步原語(信用到期時的信用)。

但是,除非你使用volatile,編譯器可以自由地將寄存器中的共享數據緩存任意長度的時間......如果你希望你的數據被寫入可預測地寫入實際內存而不是緩存在一個由編譯器自行決定的寄存器,您需要將其標記爲volatile。或者,如果您只是在修改功能後才訪問共享數據,則可能沒有問題。但我會建議不要盲目運氣,以確保值從寄存器寫回內存。

特別是在寄存器豐富的機器上(即不是x86),變量可以在寄存器中存活很長時間,而好的編譯器甚至可以緩存寄存器中的部分結構或整個結構。所以你應該使用volatile,但爲了性能,還要將值複製到局部變量進行計算,然後進行顯式回寫。從本質上講,有效使用volatile將意味着在你的C代碼中做一些加載存儲的思考。

在任何情況下,您都必須使用某種OS級別提供的同步機制來創建正確的程序。

有關volatile的弱點的示例,請參閱我的Decker算法示例http://jakob.engbloms.se/archives/65,這很好地證明了volatile不能同步。

+3

將變量長時間保存在寄存器中正是編譯器優化程序的要點。使用volatile會完全否定這一點。請注意,在GCC(也可能是大多數編譯器)函數調用clobber內存,這意味着如果你寫入非局部變量然後做一個函數調用,編譯器不允許在函數調用後推寫 - 這看起來是什麼無論如何,你的意圖是使用volatile。這不是什麼易變的... – 2009-04-24 17:11:41

0

易失性意味着我們必須去記憶來獲取或設置此值。如果你不設置volatile,編譯後的代碼可能會長時間將數據存儲在寄存器中。

這意味着你應該將線程之間共享的變量標記爲volatile,這樣就不會有一個線程開始修改值但不會在第二個線程出現之前寫入結果並嘗試閱讀價值。

Volatile是一種編譯器提示,禁用某些優化。如果沒有它,編譯器的輸出組件可能是安全的,但是您應該始終將它用於共享值。

如果您不使用系統提供的昂貴的線程同步對象,那麼這一點尤其重要 - 例如,您可能有一個數據結構,您可以通過一系列原子更改使其保持有效。很多不分配內存的堆棧就是這樣的數據結構的例子,因爲你可以向堆棧中添加一個值,然後移動結束指針或者在移動結束指針後從堆棧中移除一個值。在實現這樣的結構時,volatile會變得非常重要,以確保您的原子指令實際上是原子性的。

+1

雖然volatile不能保證原子性。它用於指示程序外部正在修改變量的內容。 – Allen 2008-09-16 23:17:14

+1

即使有揮發性,就像「a = a + 1;」一樣簡單不是原子的。這只是意味着編譯器會爲此操作重新加載'a',並立即將其存回。還有一個窗口可以讓另一個線程參加比賽。 – bdonlan 2009-05-18 00:22:38

2

以我的經驗,沒有;當你寫這些值時,你只需要適當地互斥自己,或者構造你的程序,以便線程在需要訪問依賴於另一個線程的動作的數據之前停止。我的項目x264使用這種方法;線程共享大量的數據,但絕大多數數據不需要互斥鎖,因爲它的只讀或線程將等待數據在需要訪問之前變爲可用並最終完成。現在,如果你有很多線程在操作中都被嚴重交錯(它們在很細的層次上依賴於彼此的輸出),這可能會更難 - 事實上,在這樣的一個線程中case我會考慮重新審視線程模型,看看它是否可以更乾淨地完成線程之間的更多分離。

25

只要你使用鎖來控制對變量的訪問,你就不需要volatile了。事實上,如果你把變量放在任何變量上,你可能已經錯了。

https://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming/

+0

感謝您的回答;但是關於你沒有使用鎖的情況怎麼樣(參考編輯過的問題的例子)。 – fuad 2008-09-17 03:41:52

+0

我認爲這實際上是錯誤的,請參閱下面的回覆。問題是編譯器可以做任何它喜歡的事情來保持線程中的本地寄存器的值,除非它被標記爲易失性。所以需要易失性來確保將數據寫回內存。 – jakobengblom2 2008-10-03 13:13:50

+2

如果你不使用鎖,你幾乎肯定需要使用顯式的內存屏障。請注意,volatile不是一個內存屏障,因爲它不會影響除volatile變量本身以外的其他任何加載和存儲。這也常常是一種悲觀。 – bdonlan 2009-05-18 00:21:27

0

揮發,如果你需要當一個線程寫的東西,另一個線程讀取它絕對沒有延遲只會是有用的。但是,如果沒有某種鎖定,您不知道另一個線程寫入數據,只是它是最新的可能值。

對於簡單的值(int和浮點大小不同),如果不需要明確的同步點,互斥可能會矯枉過正。如果您不使用某種類型的互斥鎖或鎖,則應聲明變量volatile。如果你使用互斥量,你就全都設置好了。

對於複雜的類型,您必須使用互斥鎖。對它們的操作是非原子的,所以你可以讀一個沒有互斥體的半改版本。

1

NO。

Volatile僅在讀取可以獨立於CPU讀取/寫入命令而改變的存儲器位置時是必需的。在線程的情況下,CPU完全控制每個線程對存儲器的讀/寫,因此編譯器可以假定存儲器是連貫的並且優化CPU指令以減少不必要的存儲器訪問。

volatile的主要用途是用於訪問內存映射I/O。在這種情況下,底層設備可以獨立於CPU更改內存位置的值。如果在此條件下不使用volatile,則CPU可能會使用先前緩存的內存值,而不是讀取新更新的值。

9

答案絕對,毫不含糊,沒有。除了正確的同步原語外,您不需要使用'volatile'。所有需要完成的事情都是由這些原語完成的。

使用'volatile'既不必要也不足夠。沒有必要,因爲正確的同步原語就足夠了。這是不夠的,因爲它只會禁用一些優化,而不是所有可能會讓你感到困擾的優化。例如,它不能保證其他CPU的原子性或可見性。

但是除非你使用volatile,編譯器可以自由地將寄存器中的共享數據緩存任意長度的時間......如果你希望你的數據被寫入可預測地寫入實際內存而不僅僅是由編譯器自行決定將其緩存在寄存器中,您需要將其標記爲volatile。或者,如果您只是在修改功能後才訪問共享數據,則可能沒有問題。但我會建議不要盲目運氣,以確保值從寄存器寫回內存。

沒錯,但即使您確實使用volatile,CPU也可以自由將共享數據緩存在寫入發佈緩衝區中任意時間長度。可以咬你的一組優化與「volatile」禁用的一組優化不完全相同。所以如果你使用'volatile',你依靠盲目運氣。另一方面,如果使用具有定義的多線程語義的同步原語,則可以保證事情能夠正常工作。作爲一個優點,你不會承受'揮發性'的巨大表現。那麼爲什麼不這樣做呢?

-1

我不明白。 同步原語如何強制編譯器重新加載變量的值? 爲什麼不使用它已有的最新版本?

易失性意味着變量在代碼範圍之外更新,因此編譯器無法假定它知道它的當前值。 即使內存障礙也沒有用,因爲對內存障礙沒有影響的編譯器(對?)可能仍然使用緩存值。

-1

有些人顯然認爲編譯器會將同步調用視爲內存障礙。 「Casey」假設只有一個CPU。

如果同步原語是外部函數,並且所涉及的符號在編譯單元外部是可見的(全局名稱,導出的指針,可能會修改它們的導出函數),那麼編譯器會將它們或任何其他外部函數調用 - 作爲與所有外部可見物體相關的記憶圍欄。

否則,你是你自己的。揮發性可能是使編譯器生成正確,快速代碼的最佳工具。它通常不會是可移植的,當你需要volatile時,它實際上對你的影響取決於系統和編譯器。

-2

線程間共享的變量應聲明爲「volatile」。這告訴 編譯器,當一個線程寫入這些變量時,寫入應該到內存 (而不是寄存器)。

2

有一個普遍的觀點,即關鍵字volatile對於多線程編程是有利的。

漢斯貝姆points out有隻有三個揮發性便攜用途:

  • 揮發性可用於標記在同一範圍內的局部變量作爲setjmp的,其值應當在longjmp的被保留。目前還不清楚這種用途的哪一部分會減慢,因爲如果沒有辦法共享所討論的局部變量,原子性和排序約束就不起作用。 (甚至不清楚這種用途的哪一部分將通過要求所有變量保留在longjmp中而減慢,但是這是一個單獨的問題並且在這裏不考慮)。
  • 易失性可能在變量可能被使用時是「外部修改」的,但修改實際上是由線程本身同步觸發的,例如因爲底層內存映射在多個位置。
  • A 易失性 sigatomic_t可用於以受限制的方式與同一線程中的信號處理程序進行通信。可以考慮削弱對sigatomic_t案件的要求,但這似乎相當違反直覺。

如果你是多線程速度起見,放慢代碼絕對不是你想要的。對於多線程編程,有兩個關鍵的問題是揮發性常常誤以爲解決:

  • 原子
  • 內存一致性,即由另一個線程所看到的一個線程的操作順序。

我們首先處理(1)。易失性不保證原子讀取或寫入。例如,在大多數現代硬件上,對129位結構的易失性讀取或寫入不會成爲原子。在大多數現代硬件上,32位int的volatile讀或寫是原子的,但是volatile與它無關。。它可能是原子而沒有波動。原子性是編譯器的奇想。 C或C++標準中沒有什麼要求它是原子的。

現在考慮問題(2)。有時程序員認爲volatile會關閉volatile存取的優化。這在實踐中基本上是真實的。但這只是不穩定的訪問,而不是非易失性訪問。考慮這個片段:

volatile int Ready;  

    int Message[100];  

    void foo(int i) {  

     Message[i/10] = 42;  

     Ready = 1;  

    } 

它試圖做一些多線程編程非常合理的:寫一條消息,然後將其發送到另一個線程。另一個線程將等待,直到Ready變爲非零,然後讀取Message。嘗試使用gcc 4.0或icc將其編譯爲「gcc -O2 -S」。兩者都會首先將商店做好準備,因此它可以與i/10的計算重疊。重新排序不是編譯器錯誤。這是一個積極的優化工作。

您可能會認爲解決方案是標記所有內存引用不穩定。這簡直是​​愚蠢的。正如前面的引言所說,它只會減慢你的代碼。最糟糕的是,它可能無法解決問題。即使編譯器不重新排序引用,硬件也可能會這樣。在這個例子中,x86硬件不會對它重新排序。 Itanium(TM)處理器也不會,因爲Itanium編譯器會爲易失性存儲插入內存屏蔽。這是一個聰明的Itanium擴展。但Power(TM)等芯片將重新訂購。你真正需要訂購的是內存圍欄,也叫做內存屏障。內存圍欄可防止對圍欄進行內存操作的重新排序,或者在某些情況下,防止重新排列在一個方向上。易失性與內存隔離無關。

那麼多線程編程的解決方案是什麼?使用實現原子和柵欄語義的庫或語言擴展。按照預期使用時,庫中的操作將插入正確的柵欄。一些例子:

  • POSIX線程
  • 的Windows(TM)螺紋
  • OpenMP的
  • TBB

基於article by Arch Robison (Intel)

-1

首先,volatile是ñ沒有必要。有許多其他操作提供了保證的多線程語義,但不使用volatile。這些包括原子操作,互斥鎖等。

其次,volatile是不夠的。對於聲明爲volatile的變量,C標準不提供關於多線程行爲的任何保證。

因此既不必要也不足夠,使用它沒有多少意義。

一個例外是特定的平臺(如Visual Studio),它具有記錄多線程語義。

0

其根本原因是C語言的語義是基於一個單線程抽象機。只要程序在抽象機器上的「可觀察行爲」保持不變,編譯器就有權轉換程序。它可以合併相鄰或重疊的內存訪問,重做內存訪問多次(例如,在寄存器溢出時),或者如果它認爲程序的行爲,則簡單地丟棄內存訪問,當在單個線程中執行的不改變。因此,正如您可能懷疑的那樣,如果程序實際上應該以多線程方式執行,則行爲更改。

正如保羅·麥肯尼在一個著名的Linux kernel document指出:

它_must_not_假設編譯器會做你想要 有()和 WRITE_ONCE()不受READ_ONCE保護內存引用什麼。如果沒有它們,編譯器有權利對 進行各種「創意」轉換,這些轉換在編譯器屏障部分的 中進行了介紹。

READ_ONCE()和WRITE_ONCE()被定義爲引用變量上的易變轉換。因此:

int y; 
int x = READ_ONCE(y); 

等同於:

int y; 
int x = *(volatile int *)&y; 

所以,除非你做一個「揮發」訪問,你不放心,發生訪問恰好一次,無論什麼同步機制,你正在使用。調用外部函數(例如pthread_mutex_lock)可能會強制編譯器對內存訪問全局變量。但是這隻有在編譯器無法確定外部函數是否更改這些全局變量時纔會發生。採用先進的程序間分析和鏈接時間優化的現代編譯器使得這個技巧無用。

總之,您應該標記多個線程共享的變量volatile或使用volatile轉換訪問它們。


正如保羅·麥肯尼還指出:

我看到他們眼中的閃爍,當他們討論你不希望你的孩子瞭解優化技巧!


但看看會發生什麼C11/C++ 11