以下所描述的volatile關(guān)鍵字僅僅針對(duì)C/C++語(yǔ)言中的,并不適用于其他語(yǔ)言.
volitate
一個(gè)定義為volatile的變量是說(shuō)這變量可能會(huì)被意想不到地改變勇婴,這樣焚志,編譯器就不會(huì)去優(yōu)化這個(gè)變量的值了。精確地說(shuō)就是胆筒,優(yōu)化器在用到這個(gè)變量時(shí)必須每次都小心地重新讀取這個(gè)變量的值邮破,而不是使用保存在寄存器里的備份诈豌。下面是volatile變量的幾個(gè)例子:
- 硬件寄存器(如:狀態(tài)寄存器)
- 一個(gè)中斷服務(wù)子程序中會(huì)訪問(wèn)到的非自動(dòng)變量(Non-automatic variables)
- 多線程應(yīng)用中被幾個(gè)任務(wù)共享的變量
在多線程的程序中,共同訪問(wèn)的內(nèi)存當(dāng)中抒和,多個(gè)程序都可以操縱這個(gè)變量,你自己的程序是無(wú)法判定何時(shí)這個(gè)變量會(huì)發(fā)生變化.例如A線程將變量復(fù)制到寄存器中,然后進(jìn)入循環(huán)反復(fù)檢測(cè)寄存器中的值是否滿足一定的條件(它期待B線程改變變量的值),此時(shí)B改變了變量的值,但是這個(gè)改變對(duì)于A線程已經(jīng)復(fù)制到寄存器中的值并沒(méi)有影響,于是A就永遠(yuǎn)處于死循環(huán)狀態(tài).
問(wèn)題
一個(gè)參數(shù)既可以是const還可以是volatile嗎矫渔?解釋為什么。
答:可以是摧莽。一個(gè)例子是只讀的狀態(tài)寄存器庙洼。它是volatile因?yàn)樗赡鼙灰庀氩坏降馗淖儭K莄onst因?yàn)槌绦虿粦?yīng)該試圖去修改它镊辕。一個(gè)指針可以是volatile 嗎油够?解釋為什么。
答:可以征懈。盡管這并不很常見(jiàn)叠聋。一個(gè)例子是當(dāng)一個(gè)中斷服務(wù)子程序修改一個(gè)指向一個(gè)buffer的指針時(shí)。下面的函數(shù)有什么錯(cuò)誤:
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
這段代碼的目的是用來(lái)返回指針*ptr指向值的平方受裹,但是碌补,由于*ptr指向一個(gè)volatile型的參數(shù),編譯器將會(huì)產(chǎn)生類似于下面的代碼:
int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
由于*ptr的值可能會(huì)被意想不到的改變棉饶,因此a和b可能不是一樣的值厦章。結(jié)果,這段代碼可能會(huì)返回不是你所期望的平方值照藻。正確代碼如下:
long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}
當(dāng)要求使用volatile聲明的變量的值的時(shí)候袜啃,系統(tǒng)總是重新從它所在的內(nèi)存讀取數(shù)據(jù),即使它前面的指令剛剛從該處讀取過(guò)數(shù)據(jù)幸缕,而且讀取的數(shù)據(jù)立刻被保存.
volatile int i = 10;
int a=i;
...//其他代碼,并未明確告訴編譯器,對(duì)i進(jìn)行過(guò)操作
int b =i;
volatile指出i是隨時(shí)可能發(fā)生變化的群发,每次使用它的時(shí)候必須從i的地址中讀取,因而編譯器生成的匯編代碼會(huì)重新從i的地址讀取數(shù)據(jù)放在b中发乔。而優(yōu)化做法是熟妓,由于編譯器發(fā)現(xiàn)兩次從i讀數(shù)據(jù)的代碼之間的代碼沒(méi)有對(duì)i進(jìn)行過(guò)操作,它會(huì)自動(dòng)把上次讀的數(shù)據(jù)放在b中栏尚。而不是重新從i里面讀起愈。這樣以來(lái),如果i是一個(gè)寄存器變量或者表示一個(gè)端口數(shù)據(jù)就容易出錯(cuò)译仗,所以說(shuō)volatile可以保證對(duì)特殊地址的穩(wěn)定訪問(wèn)抬虽。
另外一個(gè)例子是:for(int i =0; i<100000;i++)
這個(gè)語(yǔ)句是用來(lái)測(cè)試空循環(huán)的速度的,但是編譯器肯定會(huì)把它優(yōu)化掉,根本就不執(zhí)行.但是如果你寫成for(volatile int i=0;i<100000;i++);
他就會(huì)執(zhí)行了.
在多線程數(shù)據(jù)同步中的作用
無(wú)鎖的共享數(shù)據(jù)
如果不用鎖,多個(gè)線程共享的數(shù)據(jù)使用需要非常小心纵菌,如下面這個(gè)例子 :
int gCounter;
void Increment(void) { gCounter++; }
int GetCurrent(void) { return gCounter; }
多個(gè)線程同時(shí)調(diào)用 Increment 并不能保證安全阐污,因?yàn)?gCounter++ 會(huì)被分成三步原子操作 :
- 將當(dāng)前值讀入寄存器
- 寄存器自增
- 將寄存器的值寫回內(nèi)存
volatile并不能夠解決上述這種情況.考慮下面的代碼:
struct SharedDataStructure gSharedStructure;
int gFlag;
//線程A
gSharedStructure.foo = ...;
gSharedStructure.bar = ...;
gSharedStructure.baz = ...;
gFlag = 1;
...
//線程B
if(gFlag)
UseSharedStructure(&gSharedStructure;);
...
在上面的這一段代碼當(dāng)中,結(jié)構(gòu)體和flag之間因?yàn)椴淮嬖谝蕾囮P(guān)系,執(zhí)行的順序可能會(huì)被編譯器修改(compiler reorder).線程A的代碼可能是flag先被置為1,然后處理結(jié)構(gòu)體,這樣線程 B 的 if 語(yǔ)句內(nèi)就是不安全的 。這種情況下咱圆,可以將gFlag和gSharedStructure 都加上 volatile 關(guān)鍵字笛辟,保證編譯器按照代碼順序產(chǎn)生機(jī)器碼功氨。
CPU Memory Reordering
但是在加了 volatile 關(guān)鍵字之后,上述代碼在實(shí)際執(zhí)行時(shí)仍不能保證安全隘膘,因?yàn)?CPU 會(huì)激進(jìn)的 reorder 以加快執(zhí)行速度, 因此內(nèi)部執(zhí)行機(jī)器碼的順序仍然是未知的杠览,只保證最終結(jié)束時(shí)的結(jié)果與原順序一致(as-if) 弯菊。在這種情況下,如果線程 A 和 B 被分配到兩個(gè) CPU 執(zhí)行踱阿,B 的 CPU 實(shí)際并不知道 A 的 CPU 執(zhí)行情況管钳。 因此仍然存在 gFlag 被先修改而 gSharedStructure 未處理的問(wèn)題。此時(shí)可以用 OSMemoryBarrier (libkern/OSAtomic.h)來(lái)解決問(wèn)題软舌,代碼修改如下:
//線程A
gSharedStructure.foo = ...;
gSharedStructure.bar = ...;
gSharedStructure.baz = ...;
OSMemoryBarrier();
gFlag = 1;
...
//線程B
if(gFlag) {
OSMemoryBarrier();
UseSharedStructure(&gSharedStructure;);
}
這是gSharedStructure 已經(jīng)不需要再加 volatile 了 而 gFlag 還要分具體情況
while(1) {
if(gFlag) {
OSMemoryBarrier();
UseSharedStructure(&gSharedStructure;);
}
}
如果是上面這種情形才漆,仍然需要加 volatile,因?yàn)樵谶@個(gè)代碼段里面沒(méi)有修改 gFlag佛点,編譯器就會(huì)只讀一次 gFlag 值放在緩存里一直用了醇滥。
再看另一種情況 :
- (void)method {
if(gFlag) {
OSMemoryBarrier();
UseSharedStructure(&gSharedStructure;);
}
}
這種情況就不需要再給 gFlag 加 volatile 了,因?yàn)榇a每次是從外部調(diào)進(jìn)來(lái)(foreign code)超营,每次都會(huì)重新讀取 gFlag 鸳玩。(仍需注意,有可能因?yàn)?inline 或者整體的優(yōu)化導(dǎo)致不是 foreign code) 演闭。
需要使用 volatile 的另外一個(gè)例子:
int gCount;
// Thread A:
while(!done) {
work();
gCount++;
}
// Thread B:
while(gCount < total) ;
但這里仍然有個(gè)例外不跟,如果聲明的是 volatile int64_t gCount 而實(shí)際運(yùn)行在 32-bit CPU 上,CPU 一次原子操作并不能完成變量的讀/寫米碰,因而仍然不能保證安全窝革。
最后,即使已經(jīng)熟知 volatile 種種特性吕座,實(shí)際仍不推薦使用虐译,因?yàn)楦鞣N編譯器的優(yōu)化器也可能存在bug.
結(jié)論:不要預(yù)期在多線程中使用volatile來(lái)解決數(shù)據(jù)的同步問(wèn)題,該加鎖時(shí)就加鎖
- volatile 在循環(huán)中讀寫一個(gè)共享值時(shí)非常有用,如果循環(huán)中沒(méi)有 foreign code
- volatile 并不能有效保證多段代碼的執(zhí)行順序吴趴,應(yīng)用 OSMemoryBarrier 解決
- volatile 在多線程場(chǎng)景下菱蔬,對(duì)那些不能被 CPU 進(jìn)行原子讀寫的變量并沒(méi)有用(32bit/64bit)
- volatile 對(duì)有鎖或者有其它原子操作方案的復(fù)雜類型共享數(shù)據(jù)來(lái)說(shuō)沒(méi)有用
- 即使代碼能夠完美使用了 volatile,也不能保證編譯器不出 bug史侣。拴泌。