volatile
關鍵字的目的是防止編譯器對變量訪問作任何優化,由於這些變量可能會以編譯器沒法肯定的方式被修改。html
聲明爲volatile
的變量不會被優化,由於它們的值隨時可能被當前代碼範圍以外的代碼修改。系統老是從內存讀取變量的當前值,而不會使用寄存器中的值,即便上條指令剛操做過此數據。(volatile
的影響遠不止是否使用寄存器值這麼簡單)程序員
當變量的值可能發生意外變化時,應該將其聲明爲volatile。實際上,只有三種狀況:objective-c
第一種狀況,外圍設備寄存器的值隨時可能被外部改變,顯然超出了代碼的範圍。第二種狀況,中斷處理程序的執行模式不一樣於普通程序,當中斷到來時,當前線程掛起,執行中斷處理程序,以後恢復代碼的執行。能夠認爲,中斷處理程序與當前程序是並行的,獨立於正常代碼執行序列以外。第三種狀況比較常見,就是通常的併發編程。編程
編譯器假設變量值變化的惟一方式是被代碼修改。多線程
int a = 24;
複製代碼
如今編譯器會認爲 a
的值一直是24
,除非遇到修改a
值的語句。若是後面有代碼:併發
int b = a + 3;
複製代碼
編譯器會認爲,既然已經知道a
的值是24
,所以b
的值確定是27
,因此不須要生成計算a + 3
的指令。app
若是a
的值在兩條語句中間被修改了,那麼編譯的結果就會出錯。然而,a
的值爲何會忽然被修改呢?不會的。函數
若是a
是一個棧變量,除非傳遞一個指向它的引用,不然它的值是不會改變的。例如:工具
doSomething(&a);
複製代碼
函數doSomething
有一個指向a
的指針,意味着a
的值可能會被修改,此行代碼以後a
的值可能就再也不是24了。若是這樣寫:oop
int a = 24;
doSomething(&a);
int b = a + 3;
複製代碼
編譯器將不會優化掉a + 3
的計算。誰知道doSomething
以後a
的值是多少呢?編譯器顯然不知道。
對於全局變量或者對象的實例變量,問題會更復雜一些。這些變量不在棧上,而是在堆中,這意味着不一樣的線程能夠訪問它們。
// Global Scope
int a = 0;
void function() {
a = 24;
b = a + 3;
}
複製代碼
b
會是27
嗎?極可能是的,不過其它線程也可能在兩條語句之間修改了a
的值,儘管這種可能性比較小。編譯器會意識到這一點兒嗎?不會的。由於C語言自己並不知道關於線程的任何東西——至少過去是這樣的(最新的C標準終於知道了native線程,不過以前全部的線程功能都是由操做系統提供的API,而不是C語言自己的特性)。所以C編譯器依然會認爲b
的值是27,並將計算優化掉,這會致使錯誤的結果。
這就是volatile
的用武之地了。若是標記變量爲volatile:
volatile int a = 0;
複製代碼
咱們告訴編譯器:a
的值可能隨時會忽然改變。對於編譯器來講,這意味着它不能假設a
的值,哪怕1皮秒以前它仍是那個值,而且看起來也沒有代碼修改它。每次訪問a
時,老是讀取它的當前值。
過分使用volatile會阻礙許多編譯器優化,可能會顯著下降計算代碼的速度,並且人們常常在沒必要要的狀況下使用volatile。例如,編譯器不會跨越內存屏障進行值假設。內存屏障是什麼超出了本文的討論範圍,只須要知道典型的同步結構都是內存屏障,例如鎖、互斥或信號量等。對於下面代碼:
// Global Scope
int a = 0;
void function() {
a = 24;
pthread_mutex_lock(m);
b = a + 3;
pthread_mutex_unlock(m);
}
複製代碼
pthread_mutex_lock
是一個內存屏障(pthread_mutex_unlock
也是),所以不須要將a
聲明爲volatile
,編譯器不會跨越內存屏障假設a
的值,永遠不會。
Objective-C在全部方面都很像C,畢竟它只是一個帶有運行時的擴展版的C。須要指出的一點是,atomic
屬性是內存屏障,所以不須要爲屬性聲明volatile。若是須要在多個線程中訪問屬性,那麼能夠將屬性聲明爲atomic
的(若是不聲明nonatomic
,默認也是atomic
)。若是不須要在多個線程訪問,標記爲nonatomic
會使屬性的訪問更快,不過只有在頻繁訪問屬性時纔會表現出來(不是指一分鐘訪問10次那種,而是一秒鐘須要訪問數千次以上)。
Obj-C代碼何時須要使用volatile呢?
@implementation SomeObject {
volatile bool done;
}
- (void)someMethod {
done = false;
// Start some background task that performes an action
// and when it is done with that action, it sets `done` to true.
// ...
// Wait till the background task is done
while (!done) {
// Run the runloop for 10 ms, then check again
[[NSRunLoop currentRunLoop]
runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]
];
}
}
@end
複製代碼
若是沒有volatile
,編譯器可能會愚蠢地認爲,done
不會改變,所以簡單地用true
替換done
,以至於造成一個死循環。
當Apple還在使用GCC 2.x時,若是上面的代碼沒有使用volatile,的確會致使死循環(僅在開啓優化的release編譯模式,debug模式並不會)。在現代編譯器上沒有驗證過這一點,或許當前版本的clang
更智能一些。不過咱們顯然不能期望編譯器足夠聰明來正確處理這一點。 同時也取決於啓動後臺任務的方式。若是dispatch一個block,編譯器很容易知道done
是否會被改變。若是向某處傳遞一個指向done
的指針,編譯器知道done
的值可能會被修改,所以不會對其值進行任何假設。
若是你看過蘋果在<libkern/OSAtomic.h>
文件或atomic 的手冊頁中提供的原子操做,那麼或許你已經注意到,每一個操做有兩個版本:一個x
和一個xBarrier
(例如,OSAtomicAdd32
和OSAtomicAdd32Barrier
)。如今你知道了,名字中帶有「Barrier」的是一個內存屏障,而另外一個不是。
內存屏障不只適用於編譯器,也適用於CPU(有些CPU指令被認爲是內存屏障)。CPU須要知道這些屏障,由於CPU會對指令從新排序,以便流水線化亂序執行。例如:
a = x + 3; // (1)
b = y * 5; // (2)
c = a + b; // (3)
複製代碼
假設加法器的流水線正忙,而乘法器的流水線還有空閒,那麼CPU可能會在(1)以前先執行(2),畢竟執行順序並不會影響最終的運算結果。這能夠防止管道停滯。固然,CPU也足夠聰明地知道(3)的執行不能早於(1)或(2),由於(3)的結果依賴於(1)和(2)的結果。
流水線,簡單來講就是一個CPU核心有多套運算器,每條指令分爲幾個階段,多條指令並行執行。
load instruction decode load instruction load data decode load instruction operation load data decode save data operation load data save data operation save data
然而,某些類型的順序更改會破壞代碼或程序員的意圖。考慮以下代碼:
x = y + z; // (1)
a = 1; // (2)
複製代碼
加法器流水線正忙,所以爲何不在(1)以前先執行(2)呢?它們沒有依賴關係,所以順序可有可無,對吧?看狀況。假設有一個線程正在監聽a
的變化,當a
變爲1時,讀取x
的值,若是按序執行的話值應該是y + z
。但若是CPU調整了執行順序,x
的值就仍是此段代碼執行以前的值,此時另外一個線程獲取到的值是不符合程序員指望的。
對於這種狀況,順序是很重要的,這就是爲何CPU也須要屏障:CPU不會跨越屏障從新排序指令。所以,指令(2)須要是一個屏障指令(或者在(1)和(2)之間有一個屏障指令,取決於具體的CPU)。
從新排序指令是現代CPU的特性,還有一個更老的問題是延遲內存寫操做。若是CPU延遲對內存的寫操做(對於一些CPU來講很常見,由於內存訪問速度相對於CPU來講實在太慢了),它將確保全部延遲的寫操做在跨越內存屏障以前被執行並完成,所以當其它線程訪問時,全部的內存都處於正確的狀態(知道「內存屏障」這個詞的出處了吧)。
與內存屏障打交道的地方可能比咱們意識到的要多不少(GCD - Grand Central Dispatch處處都是內存屏障,以及基於GCD的NSOperation/NSOperationQueue
),這就是爲何咱們只須要在很是少的、特殊的狀況才真正須要使用volatile
。可能你寫了100個App都不須要用到一次。然而,若是咱們須要編寫大量低層的、多線程的代碼,並指望達到最高的性能,那麼或早或晚會遇到必須使用volatile
才能確保功能正確的狀況。若是此種狀況不使用volatile,可能致使死循環或變量值不正確卻沒法解釋的問題。若是遇到了這樣的問題,尤爲是隻在release模式下才會出現,那麼極可能是由於缺失了volatile
或內存屏障。
爲了優化代碼性能,編譯器默認狀況下會根據當前代碼的上下文推斷變量的值,以減小沒必要要的計算。在單線程、正常執行的狀況下,不會有什麼問題。可是在中斷處理程序、多線程併發和內存映射I/O的狀況,變量的值可能在當前代碼範圍以外忽然被修改,這些狀況超出了編譯器的意識範圍。所以,須要咱們顯式地告訴編譯器,不要推斷這些變量的值,由於它們隨時可能被當前代碼範圍以外的代碼或硬件修改。
另外,編譯器不會跨越內存屏障推斷變量的值。在實際編程中,不少內存屏障是隱性的,由於常見的同步工具已帶有內存屏障功能,如鎖、互斥和信號量等,iOS並行編程中最經常使用的GCD處處都是內存屏障,atomic
屬性也是一個內存屏障。
屏障不只適用於編譯器,也適用於CPU。絕大多數現代CPU都引入了流水線,亂序並行執行多條指令。當咱們想要確保指令執行順序時,也須要使用屏障指令。CPU不會跨越屏障重排指令順序。
須要注意的是,C語言自己並無線程的概念,線程是操做系統提供的API,所以編譯器不會假設全局變量隨時會被其它線程修改。固然,編譯器的智能化在不斷提升,C語言自己也在進化。不過,咱們仍是不要依賴於編譯器的聰明程度爲好。