去年年底的樣子,何登成寫了一篇關於C/C++ volatile關鍵字的深度剖析blog(C/C++ Volatile關鍵詞深度剖析)。全文深入分析了volatile關鍵字的三個特性。這里不想就已有內容再做一遍重復,而是再提供一些自己的看法,以完善對volatile的全面認識。
前文一個很好的例子就是:
在這個例子里事實上還引入的另外一個問題,就是多線程環境里該如何使用volatile?
要全面回答這個問題,沒那么容易。不過一個已經被很多人接受的結論已經有了,並且很具有權威性。這個結論來自於Linux kernel documention。
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,除非能給出強有力的證據!所以我們的項目中,幾乎可以肯定,根本沒有使用的必要。
結論已經有了,接下來就是闡述為什么了。
在多線程中使用volatile,很多情況下就是為了解決共享數據的訪問問題。比方說上面這個例子,如果不使用volatile,那么編譯器生成的代碼在訪問flag變量時,很可能都是從緩存(寄存器)中讀取的。某個線程對flag的修改,無法通知到另外一個線程。為此需要使用volatile,保證每次讀寫都需要有內存訪問。這體現了volatile的易變性和不可優化性。與此同時,也引出了一個疑問:volatile的這些特性確信是解決該問題的正確方案么,或者說就沒有其他可選的解決方案了么?
顯然volatile不能解決這個問題,因為還存在編譯器優化和CPU執行指令時的亂序情況(Out-of-Order Execution, OOE。不過上面這個例子在x86-based機器上不會發生OOE的情況,可以看這里了解x86-based CPU亂序的總結)。
所以說,多線程下訪問共享數據至少要考慮兩點(應該還有其他要考慮的,但是寫到這里,我只能列這些):
- 數據一致性。保證每次讀到的都是最新的數據,每次寫都是基於最新的數據。
- 指令執行在某種程度上的順序性。
而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:
- Why the “volatile” type class should not be used
- doc: volatile considered evil
- Lockless Programming Considerations for Xbox 360 and Microsoft Windows
- volatile (C++) 2013
- volatile (C++) 2005
- Optimization of conditional access to globals: thread-unsafe?
- Single Threaded Memory Model
- -fno-tree-cselim not working?