去年年末的樣子,何登成寫了一篇關於C/C++ volatile關鍵字的深度剖析blog(C/C++ Volatile關鍵詞深度剖析)。全文深刻分析了volatile關鍵字的三個特性。這裏不想就已有內容再作一遍重複,而是再提供一些本身的見解,以完善對volatile的全面認識。html
前文一個很好的例子就是:linux
在這個例子裏事實上還引入的另一個問題,就是多線程環境裏該如何使用volatile?編程
要全面回答這個問題,沒那麼容易。不過一個已經被不少人接受的結論已經有了,而且很具備權威性。這個結論來自於Linux kernel documention。windows
C programmers have often taken volatile to mean that the variable could be
changed outside of the current thread of execution; as a result, they are
sometimes tempted to use it in kernel code when shared data structures are
being used. In other words, they have been known to treat volatile types
as a sort of easy atomic variable, which they are not. The use of volatile in
kernel code is almost never correct.緩存
說明白點就是,在linux kernel這種大型而且複雜的系統編程項目裏,不能使用volatile,除非能給出強有力的證據!因此咱們的項目中,幾乎能夠確定,根本沒有使用的必要。多線程
結論已經有了,接下來就是闡述爲何了。app
在多線程中使用volatile,不少狀況下就是爲了解決共享數據的訪問問題。比方說上面這個例子,若是不使用volatile,那麼編譯器生成的代碼在訪問flag變量時,極可能都是從緩存(寄存器)中讀取的。某個線程對flag的修改,沒法通知到另一個線程。爲此須要使用volatile,保證每次讀寫都須要有內存訪問。這體現了volatile的易變性和不可優化性。與此同時,也引出了一個疑問:volatile的這些特性確信是解決該問題的正確方案麼,或者說就沒有其餘可選的解決方案了麼?less
顯然volatile不能解決這個問題,由於還存在編譯器優化和CPU執行指令時的亂序狀況(Out-of-Order Execution, OOE。不過上面這個例子在x86-based機器上不會發生OOE的狀況,能夠看這裏瞭解x86-based CPU亂序的總結)。dom
因此說,多線程下訪問共享數據至少要考慮兩點(應該還有其餘要考慮的,可是寫到這裏,我只能列這些):ide
而volatile關鍵字根本不能保證這兩點內容。因此volatile在多線程下根本沒有用。由於volatile類型的數據並不保證數據讀寫的原子性。而且volatile關鍵字生成的代碼通常狀況下不會附帶上特殊的CPU指令。所以volatile至少不能控制CPU的亂序執行。
咱們再從一個簡單的場景來考慮這個問題。假設有多個線程會去讀寫同一個變量a。咱們一般的作法是怎麼樣的?對,使用鎖。爲何?這麼高深的問題,我只能借用別人的研究成果了:
Like volatile, the kernel primitives which make concurrent access to data
safe (spinlocks, mutexes, memory barriers, etc.) are designed to prevent
unwanted optimization.
結論來了,若是有鎖在,和鎖相關的代碼是會被特殊考慮的,不應有的優化是會被屏蔽的(應該是編譯器的代碼生成和CPU執行指令兩方面都有影響)。因此你但願volatile能作的事情(雖然它作不到),鎖或者內存屏障都能作。而且在使用這些工具的時候,根本不須要volatile的參與。
至此,關於不須要使用volatile的論證基本就結束了。
回到何的文章,後面還介紹了爲何會有volatile這個關鍵字,這個關鍵字解決了什麼問題。這裏想補充說的是,volatile關鍵字並非定義了一個和數據內容相關的屬性;volatile關鍵字是定義了一個和數據訪問相關的屬性。從當初volatile被設計爲用於MMIO(Memory Mapped IO)以及C/C++最初並不包含多線程的概念能夠看出volatile並非爲了多線程而設計的。所以將volatile應用於多線程自己就不合適。
In C, it's "data" that is volatile, but that is insane. Data
isn't volatile - _accesses_ are volatile. So it may make sense to say
"make this particular _access_ be careful", but not "make all accesses to
this data use some random strategy".
UPDATE: 2014-1-22
這裏再補充點Visual C++關於volatile關鍵字的特別之處。
Visual C++ 2005以後,volatile關鍵字和其餘高級語言,比方說C#會比較接近。直接來看MSDN的描述:
Objects declared as volatile are not used in certain optimizations because their values can change at any time. The system always reads the current value of a volatile object at the point it is requested, even if a previous instruction asked for a value from the same object. Also, the value of the object is written immediately on assignment.
Also, when optimizing, the compiler must maintain ordering among references to volatile objects as well as references to other global objects. In particular,
- A write to a volatile object (volatile write) has Release semantics; a reference to a global or static object that occurs before a write to a volatile object in the instruction sequence will occur before that volatile write in the compiled binary.
- A read of a volatile object (volatile read) has Acquire semantics; a reference to a global or static object that occurs after a read of volatile memory in the instruction sequence will occur after that volatile read in the compiled binary.
This allows volatile objects to be used for memory locks and releases in multithreaded applications.
因此Visual C++ 2005後,volatile對象能夠用做memory barrier。
固然,C++11標準後,狀況又有了新的變化。在VC沒有支持C++11標準前(VC2010及之前) ,對volatile關鍵字的描述中明確指明瞭volatile關鍵字是能夠用於解決多線程數據訪問的問題的:
The volatile keyword is a type qualifier used to declare that an object can be modified in the program by something such as the operating system, the hardware, or a concurrently executing thread.
可是C++11標準明確了volatile的定義,讓他迴歸了當初設計的本源:
A type qualifier that you can use to declare that an object can be modified in the program by the hardware.
the C++11 ISO Standard volatile keyword is different and is supported in Visual Studio when the /volatile:iso compiler option is specified. (For ARM, it's specified by default). The volatile keyword in C++11 ISO Standard code is to be used only for hardware access; do not use it for inter-thread communication. For inter-thread communication, use mechanisms such as std::atomic<T> from the C++ Standard Template Library.
UPDATE: 2014-2-11
關於memory reordering,Jeff Preshing的這篇文章值得深刻閱讀。特別是comments部分裏羅列的一些資源。這些額外的連接討論了一個很是有趣的問題,而且不一樣的人有不一樣的見解。有人認爲該用volatile解決reordering問題。
這個問題是這樣的。有以下一段代碼:
extern int v;
void f(int set_v)
{
if (set_v) v = 1;
}
GCC 3.3.4 - 4.3.0帶有O1優化的狀況下,彙編碼是:
f:
pushl %ebp
movl %esp, %ebp
cmpl $0, 8(%ebp)
movl $1, %eax
cmove v, %eax ; load (maybe)
movl %eax, v ; store (always)
popl %ebp
ret
從彙編看,即便調用f(0),也會存在一次寫v的動做。在多線程環境下,即使f(0)也要加鎖(v在多線程下是共享數據)。
這個問題的討論中,有人認爲這是編譯器bug;有人認爲v應該加上volatile修飾,這樣就不會生成這樣的彙編碼了。
結論是,這個是編譯器的bug。
完!
Reference: