C/C++ 中volatile關(guān)鍵字

以下所描述的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)題

  1. 一個(gè)參數(shù)既可以是const還可以是volatile嗎矫渔?解釋為什么。
    答:可以是摧莽。一個(gè)例子是只讀的狀態(tài)寄存器庙洼。它是volatile因?yàn)樗赡鼙灰庀氩坏降馗淖儭K莄onst因?yàn)槌绦虿粦?yīng)該試圖去修改它镊辕。

  2. 一個(gè)指針可以是volatile 嗎油够?解釋為什么。
    答:可以征懈。盡管這并不很常見(jiàn)叠聋。一個(gè)例子是當(dāng)一個(gè)中斷服務(wù)子程序修改一個(gè)指向一個(gè)buffer的指針時(shí)。

  3. 下面的函數(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í)就加鎖

  1. volatile 在循環(huán)中讀寫一個(gè)共享值時(shí)非常有用,如果循環(huán)中沒(méi)有 foreign code
  2. volatile 并不能有效保證多段代碼的執(zhí)行順序吴趴,應(yīng)用 OSMemoryBarrier 解決
  3. volatile 在多線程場(chǎng)景下菱蔬,對(duì)那些不能被 CPU 進(jìn)行原子讀寫的變量并沒(méi)有用(32bit/64bit)
  4. volatile 對(duì)有鎖或者有其它原子操作方案的復(fù)雜類型共享數(shù)據(jù)來(lái)說(shuō)沒(méi)有用
  5. 即使代碼能夠完美使用了 volatile,也不能保證編譯器不出 bug史侣。拴泌。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市惊橱,隨后出現(xiàn)的幾起案子蚪腐,更是在濱河造成了極大的恐慌,老刑警劉巖税朴,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件回季,死亡現(xiàn)場(chǎng)離奇詭異家制,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)泡一,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門颤殴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人鼻忠,你說(shuō)我怎么就攤上這事涵但。” “怎么了帖蔓?”我有些...
    開(kāi)封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵矮瘟,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我塑娇,道長(zhǎng)澈侠,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任埋酬,我火速辦了婚禮哨啃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘写妥。我一直安慰自己棘催,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布耳标。 她就那樣靜靜地躺著醇坝,像睡著了一般。 火紅的嫁衣襯著肌膚如雪次坡。 梳的紋絲不亂的頭發(fā)上呼猪,一...
    開(kāi)封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音砸琅,去河邊找鬼宋距。 笑死,一個(gè)胖子當(dāng)著我的面吹牛症脂,可吹牛的內(nèi)容都是我干的谚赎。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼诱篷,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼壶唤!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起棕所,我...
    開(kāi)封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤闸盔,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后琳省,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體迎吵,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡躲撰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了击费。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拢蛋。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蔫巩,靈堂內(nèi)的尸體忽然破棺而出谆棱,到底是詐尸還是另有隱情,我是刑警寧澤批幌,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布础锐,位于F島的核電站嗓节,受9級(jí)特大地震影響荧缘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拦宣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一截粗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鸵隧,春花似錦绸罗、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至外驱,卻和暖如春育灸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背昵宇。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工磅崭, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瓦哎。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓砸喻,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親蒋譬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子割岛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容