學(xué)C語言時(shí)有一個(gè)奇怪的關(guān)鍵字volatile,這到底有什么用呢?
volatile與編譯器
首先來看這樣一段代碼:
int busy = 1;
void wait() {
while(busy) {
;
}
}
編譯一下,注意, 這里使用O2優(yōu)化 :
讓我們仔細(xì)看看生成的這段匯編:
wait:
mov eax, DWORD PTR busy[rip]
:
test eax, eax
jne .L2
ret
busy:
1
其中L2這一段即為while循環(huán),這段指令是經(jīng)過編譯器優(yōu)化的,可以看到,決定能否跳出循環(huán)是通過檢查寄存器eax來完成的,而沒有檢查變量busy所在內(nèi)存的真實(shí)內(nèi)容。
注意,對于這段代碼來說這里的優(yōu)化是正確的,但問題是如果還有其它代碼修改了變量busy,那么這里的優(yōu)化會導(dǎo)致其它代碼對變量busy的修改根本就不能生效,就像這樣:
int busy = 1;
// 該函數(shù)在A線程中執(zhí)行
void wait() {
while(busy) {
;
}
}
// 該函數(shù)在B線程中執(zhí)行
void signal() {
busy = 0;
}
如果wait函數(shù)中while循環(huán)對應(yīng)的機(jī)器指令僅僅從寄存器中讀取數(shù)據(jù)那么即使B線程的signal函數(shù)修改了busy變量也不能讓wait函數(shù)從循環(huán)中跳出來。
如果你對busy變量使用volatile修飾,生成的指令就變成這樣了:
wait:
:
mov eax, DWORD PTR busy[rip]
test eax, eax
jne .L2
ret
busy:
1
注意看此時(shí)L2這一段,每次都從busy變量所在的內(nèi)存中讀取數(shù)據(jù)并存放在eax,然后再去判斷,這樣就能確保每次都能讀取到busy變量的最新值。
實(shí)際上你可以把寄存器eax當(dāng)做busy所在內(nèi)存的cache,當(dāng)cache(寄存器)和內(nèi)存中的數(shù)據(jù)一致時(shí)不會有任何問題,但當(dāng)cache與內(nèi)存中的數(shù)據(jù)不一致時(shí)(也就是內(nèi)存已被更新但cache保存的還是舊數(shù)據(jù)),程序的運(yùn)行往往出乎預(yù)料。
除了多線程的例子,還有一類就是signal handler以及硬件修改該變量(用C語言與硬件交互式時(shí)經(jīng)常遇到),如果編譯器生成文章開頭那樣的指令那么等待線程將檢測不到signal handler或者硬件對變量的修改。
因此在這里我們需要告訴編譯器:“不要耍小聰明,不要只從寄存器中讀數(shù)據(jù),這個(gè)變量可能在其它地方已經(jīng)被修改了,使用時(shí)從內(nèi)存中獲取最新數(shù)據(jù)”。
現(xiàn)在是時(shí)候簡單總結(jié)一下了, volatile僅僅阻止編譯器試圖去優(yōu)化對變量的讀取操作 。
volatile與多線程
一定要注意volatile僅僅確保變量的可見性,但和變量的原子訪問沒有半毛錢關(guān)系,這是兩個(gè)完全不同的任務(wù) 。
假設(shè)有一個(gè)非常復(fù)雜的結(jié)構(gòu)體struct foo:
struct data {
int a;
int b;
int c;
...
};
volatile struct data foo;
void thread1() {
foo.a = 1;
foo.b = 2;
foo.c = 3;
...
}
void thread2() {
int a = foo.a;
int b = foo.b;
int c = foo.c;
...
}
你僅僅用volatile去修飾變量foo只是確保了當(dāng)該變量被thread1修改后我們能在thread2中讀取到最新值, 但是這解決不了多線程并發(fā)讀寫需要原子訪問foo的問題 。
確保變量原子性訪問一般都采用鎖,當(dāng)使用鎖時(shí),鎖本身就包含了volatile提供能力,即,確保變量的可見性,因此當(dāng)使用鎖時(shí)沒有必要使用volatile。
volatile與memory order
有的同學(xué)可能會想如果我想用volatile修飾的變量沒有那么復(fù)雜,僅僅是一個(gè)int,就像這樣:
volatile int busy = 0;
A線程讀取busy變量,B線程更新busy變量,當(dāng)A檢測到busy變化后執(zhí)行特定操作,這樣可行嗎?既然通過volatile修飾后可以確保每次都從內(nèi)存中讀取busy,那么應(yīng)該可以這樣使用吧。
然而,計(jì)算機(jī)在概念上可能相對簡單些,但在工程實(shí)踐中是復(fù)雜的。
我們知道由于CPU與內(nèi)存之間的速度差異非常大,CPU與內(nèi)存之間有一層cache,CPU其實(shí)并沒有直接讀取內(nèi)存,cache的存在會讓問題復(fù)雜起來,限于篇幅與本文主題這里不再展開。
為優(yōu)化內(nèi)存讀寫,CPU可能會對內(nèi)存讀寫操作進(jìn)行指令重排,reordering,帶來的后果就是:假設(shè)在線程1中先后執(zhí)行第N行代碼與第N+1行代碼,但在線程2看來卻是第N+1行代碼先生效,假設(shè)X的初始值為0,Y的初始值為1:
線程1 線程2
X = 10 if (!busy)
busy = 0; Y = X;
當(dāng)線程2檢測到busy為0后讀取X的值,此時(shí)讀取到的X值可能為0。
為解決這一問題,我們需要的不是volatile,volatile解決不了reordering問題,我們需要的是內(nèi)存屏障,memory barrier。
內(nèi)存屏障是一類機(jī)器指令,該指令對處理器在該屏障指令之前與之后的內(nèi)存操作進(jìn)行了限制,確保不會出現(xiàn)重排問題。
而內(nèi)存屏障帶來的效果依然能夠涵蓋volatile提供的功能,因此也不需要volatile。
可以看到,在多線程環(huán)境下我們幾乎總是不會使用volatile關(guān)鍵字。
-
C語言
+關(guān)注
關(guān)注
180文章
7575瀏覽量
134017 -
匯編
+關(guān)注
關(guān)注
2文章
214瀏覽量
25834 -
volatile
+關(guān)注
關(guān)注
0文章
44瀏覽量
12979
發(fā)布評論請先 登錄
相關(guān)推薦
評論