在UE的多線程環境中,資源處理,渲染很多地方使用了Volatile關鍵字,自覺在並行方面知識甚少,趁空閑時機,Mark一記,轉些相關方面的文檔,學海無涯,願求之甚解。
一
(轉自http://www.cnblogs.com/yc_sunniwell/archive/2010/06/24/1764231.html)
什么是volatile
volatile提醒編譯器它后面所定義的變量隨時都有可能改變,因此編譯后的程序每次需要存儲或讀取這個變量的時候,都會直接從變量地址中讀取數據。如果沒有volatile關鍵字,則編譯器可能優化讀取和存儲,可能暫時使用寄存器中的值,如果這個變量由別的程序更新了的話,將出現不一致的現象。下面舉例說明。在DSP開發中,經常需要等待某個事件的觸發,所以經常會寫出這樣的程序:
short flag;
void test()
{
do1();
while(flag==0);
do2();
}
這段程序等待內存變量flag的值變為1(懷疑此處是0,有點疑問,)之后才運行do2()。變量flag的值由別的程序更改,這個程序可能是某個硬件中斷服務程序。例如:如果某個按鈕按下的話,就會對DSP產生中斷,在按鍵中斷程序中修改flag為1,這樣上面的程序就能夠得以繼續運行。但是,編譯器並不知道flag的值會被別的程序修改,因此在它進行優化的時候,可能會把flag的值先讀入某個寄存器,然后等待那個寄存器變為1。如果不幸進行了這樣的優化,那么while循環就變成了死循環,因為寄存器的內容不可能被中斷服務程序修改。為了讓程序每次都讀取真正flag變量的值,就需要定義為如下形式:
volatile short flag;
需要注意的是,沒有volatile也可能能正常運行,但是可能修改了編譯器的優化級別之后就又不能正常運行了。因此經常會出現debug版本正常,但是release版本卻不能正常的問題。所以為了安全起見,只要是等待別的程序修改某個變量的話,就加上volatile關鍵字。
volatile的本意是“易變的”
由於訪問寄存器的速度要快過RAM,所以編譯器一般都會作減少存取外部RAM的優化。比如:
static int i=0;
int main(void)
{
...
while (1)
{
if (i) do_something();
}
}
/* Interrupt service routine. */
void ISR_2(void)
{
i=1;
}
程序的本意是希望ISR_2中斷產生時,在main當中調用do_something函數,但是,由於編譯器判斷在main函數里面沒有修改過i,因此可能只執行一次對從i到某寄存器的讀操作,然后每次if判斷都只使用這個寄存器里面的“i副本”,導致do_something永遠也不會被調用。如果變量加上volatile修飾,則編譯器保證對此變量的讀寫操作都不會被優化(肯定執行)。此例中i也應該如此說明。
一般說來,volatile用在如下的幾個地方:
1、中斷服務程序中修改的供其它程序檢測的變量需要加volatile;
2、多任務環境下各任務間共享的標志應該加volatile;
3、存儲器映射的硬件寄存器通常也要加volatile說明,因為每次對它的讀寫都可能由不同意義;
另外,以上這幾種情況經常還要同時考慮數據的完整性(相互關聯的幾個標志讀了一半被打斷了重寫),在1中可以通過關中斷來實現,2中可以禁止任務調度,3中則只能依靠硬件的良好設計了。
volatile 的含義
volatile總是與優化有關,編譯器有一種技術叫做數據流分析,分析程序中的變量在哪里賦值、在哪里使用、在哪里失效,分析結果可以用於常量合並,常量傳播等優化,進一步可以死代碼消除。但有時這些優化不是程序所需要的,這時可以用volatile關鍵字禁止做這些優化,volatile的字面含義是易變的,它有下面的作用:
1 不會在兩個操作之間把volatile變量緩存在寄存器中。在多任務、中斷、甚至setjmp環境下,變量可能被其他的程序改變,編譯器自己無法知道,volatile就是告訴編譯器這種情況。
2 不做常量合並、常量傳播等優化,所以像下面的代碼:
volatile int i = 1;if (i > 0) ...if的條件不會當作無條件真。
3 對volatile變量的讀寫不會被優化掉。如果你對一個變量賦值但后面沒用到,編譯器常常可以省略那個賦值操作,然而對Memory Mapped IO的處理是不能這樣優化的。
前面有人說volatile可以保證對內存操作的原子性,這種說法不大准確,其一,x86需要LOCK前綴才能在SMP下保證原子性,其二,RISC根本不能對內存直接運算,要保證原子性得用別的方法,如atomic_inc。
對於jiffies,它已經聲明為volatile變量,我認為直接用jiffies++就可以了,沒必要用那種復雜的形式,因為那樣也不能保證原子性。
你可能不知道在Pentium及后續CPU中,下面兩組指令
inc jiffies
;;
mov jiffies, %eax
inc %eax
mov %eax, jiffies
作用相同,但一條指令反而不如三條指令快。
編譯器優化 C關鍵字volatile → memory破壞描述符zz
“memory”比較特殊,可能是內嵌匯編中最難懂部分。為解釋清楚它,先介紹一下編譯器的優化知識,再看C關鍵字volatile。最后去看該描述符。
1、編譯器優化介紹
內存訪問速度遠不及CPU處理速度,為提高機器整體性能,在硬件上引入硬件高速緩存Cache,加速對內存的訪問。另外在現代CPU中指令的執行並不一定嚴格按照順序執行,沒有相關性的指令可以亂序執行,以充分利用CPU的指令流水線,提高執行速度。以上是硬件級別的優化。再看軟件一級的優化:一種是在編寫代碼時由程序員優化,另一種是由編譯器進行優化。編譯器優化常用的方法有:將內存變量緩存到寄存器;調整指令順序充分利用CPU指令流水線,常見的是重新排序讀寫指令。對常規內存進行優化的時候,這些優化是透明的,而且效率很好。由編譯器優化或者硬件重新排序引起的問題的解決辦法是在從硬件(或者其他處理器)的角度看必須以特定順序執行的操作之間設置內存屏障(memory barrier),linux 提供了一個宏解決編譯器的執行順序問題。
void Barrier(void)
這個函數通知編譯器插入一個內存屏障,但對硬件無效,編譯后的代碼會把當前CPU寄存器中的所有修改過的數值存入內存,需要這些數據的時候再重新從內存中讀出。
2、C語言關鍵字volatile
C語言關鍵字volatile(注意它是用來修飾變量而不是上面介紹的__volatile__)表明某個變量的值可能在外部被改變,因此對這些變量的存取不能緩存到寄存器,每次使用時需要重新存取。該關鍵字在多線程環境下經常使用,因為在編寫多線程的程序時,同一個變量可能被多個線程修改,而程序通過該變量同步各個線程,例如:
DWORD __stdcall threadFunc(LPVOID signal)
{
int* intSignal=reinterpret_cast<int*>(signal);
*intSignal=2;
while(*intSignal!=1)
sleep(1000);
return 0;
}
該線程啟動時將intSignal 置為2,然后循環等待直到intSignal 為1 時退出。顯然intSignal的值必須在外部被改變,否則該線程不會退出。但是實際運行的時候該線程卻不會退出,即使在外部將它的值改為1,看一下對應的偽匯編代碼就明白了:
mov ax,signal
label:
if(ax!=1)
goto label
對於C編譯器來說,它並不知道這個值會被其他線程修改。自然就把它cache在寄存器里面。記住,C 編譯器是沒有線程概念的!這時候就需要用到volatile。volatile 的本意是指:這個值可能會在當前線程外部被改變。也就是說,我們要在threadFunc中的intSignal前面加上volatile關鍵字,這時候,編譯器知道該變量的值會在外部改變,因此每次訪問該變量時會重新讀取,所作的循環變為如下面偽碼所示:
label:
mov ax,signal
if(ax!=1)
goto label
3、Memory
有了上面的知識就不難理解Memory修改描述符了,Memory描述符告知GCC:
1)不要將該段內嵌匯編指令與前面的指令重新排序;也就是在執行內嵌匯編代碼之前,它前面的指令都執行完畢
2)不要將變量緩存到寄存器,因為這段代碼可能會用到內存變量,而這些內存變量會以不可預知的方式發生改變,因此GCC插入必要的代碼先將緩存到寄存器的變量值寫回內存,如果后面又訪問這些變量,需要重新訪問內存。
如果匯編指令修改了內存,但是GCC 本身卻察覺不到,因為在輸出部分沒有描述,此時就需要在修改描述部分增加“memory”,告訴GCC 內存已經被修改,GCC 得知這個信息后,就會在這段指令之前,插入必要的指令將前面因為優化Cache 到寄存器中的變量值先寫回內存,如果以后又要使用這些變量再重新讀取。
使用“volatile”也可以達到這個目的,但是我們在每個變量前增加該關鍵字,不如使用“memory”方便。
(http://baike.baidu.com/view/608706.htm百科上難得也有好文章)
volatile變量的幾個例子
推薦一個定義為volatile的變量是說這變量可能會被意想不到地改變,這樣,編譯器就不會去假設這個變量的值了。精確地說就是,優化器在用到這個變量時必須每次都小心地重新讀取這個變量的值,而不是使用保存在寄存器里的備份。下面是volatile變量的幾個例子: 1). 並行設備的硬件寄存器(如:狀態寄存器)
2). 一個中斷服務子程序中會訪問到的非自動變量(Non-automatic variables)
3). 多線程應用中被幾個任務共享的變量
回答不出這個問題的人是不會被雇佣的。我認為這是區分C程序員和嵌入式系統程序員的最基本的問題。嵌入式系統程序員經常同硬件、中斷、RTOS等等打交道,所用這些都要求volatile變量。不懂得volatile內容將會帶來災難。
假設被面試者正確地回答了這是問題(嗯,懷疑是否會是這樣),我將稍微深究一下,看一下這家伙是不是真正懂得volatile完全的重要性。
1). 一個參數既可以是const還可以是volatile嗎?解釋為什么。
2). 一個指針可以是volatile 嗎?解釋為什么。
3). 下面的函數有什么錯誤:
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
下面是答案:
1). 是的。一個例子是只讀的狀態寄存器。它是volatile因為它可能被意想不到地改變。它是const因為程序不應該試圖去修改它。
2). 是的。盡管這並不很常見。一個例子是當一個中斷服務子程序修改一個指向一個buffer的指針時。
3). 這段代碼是個惡作劇。這段代碼的目的是用來返指針*ptr指向值的平方,但是,由於*ptr指向一個volatile型參數,編譯器將產生類似下面的代碼:
int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
由於*ptr的值可能被意想不到地改變,因此a和b可能是不同的。結果,這段代碼可能返不是你所期望的平方值!正確的代碼如下:
long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}
講講我的理解: (歡迎打板子...~~!) 關鍵在於兩個地方:
1. 編譯器的優化 (請高手幫我看看下面的理解) 在本次線程內, 當讀取一個變量時,為提高存取速度,編譯器優化時有時會先把變量讀取到一個寄存器中;以后,再取變量值時,就直接從寄存器中取值; 當變量值在本線程里改變時,會同時把變量的新值copy到該寄存器中,以便保持一致 當變量在因別的線程等而改變了值,該寄存器的值不會相應改變,從而造成應用程序讀取的值和實際的變量值不一致 當該寄存器在因別的線程等而改變了值,原變量的值不會改變,從而造成應用程序讀取的值和實際的變量值不一致 舉一個不太准確的例子: 發薪資時,會計每次都把員工叫來登記他們的銀行卡號;一次會計為了省事,沒有即時登記,用了以前登記的銀行卡號;剛好一個員工的銀行卡丟了,已掛失該銀行卡號;從而造成該員工領不到工資 員工 -- 原始變量地址 銀行卡號 -- 原始變量在寄存器的備份 2. 在什么情況下會出現(如1樓所說)
1). 並行設備的硬件寄存器(如:狀態寄存器)
2). 一個中斷服務子程序中會訪問到的非自動變量(Non-automatic variables)
3)多線程應用中被幾個任務共享的變量
補充: volatile應該解釋為“直接存取原始內存地址”比較合適,“易變的”這種解釋簡直有點誤導人; “易變”是因為外在因素引起的,象多線程,中斷等,並不是因為用volatile修飾了的變量就是“易變”了,假如沒有外因,即使用volatile定義,它也不會變化; 而用volatile定義之后,其實這個變量就不會因外因而變化了,可以放心使用了; 大家看看前面那種解釋(易變的)是不是在誤導人
------------簡明示例如下:-----------------
volatile關鍵字是一種類型修飾符,用它聲明的類型變量表示可以被某些編譯器未知的因素更改,比如:操作系統、硬件或者其它線程等。遇到這個關鍵字聲明的變量,編譯器對訪問該變量的代碼就不再進行優化,從而可以提供對特殊地址的穩定訪問。 使用該關鍵字的例子如下:
int volatile nVint;
>>>>當要求使用volatile 聲明的變量的值的時候,系統總是重新從它所在的內存讀取數據,即使它前面的指令剛剛從該處讀取過數據。而且讀取的數據立刻被保存。 例如:
volatile int i=10; int a = i; ...
//其他代碼,並未明確告訴編譯器,對i進行過操作
int b = i;
>>>>volatile 指出 i是隨時可能發生變化的,每次使用它的時候必須從i的地址中讀取,因而編譯器生成的匯編代碼會重新從i的地址讀取數據放在b中。而優化做法是,由於編譯器發現兩次從i讀數據的代碼之間的代碼沒有對i進行過操作,它會自動把上次讀的數據放在b中。而不是重新從i里面讀。這樣一來,如果i是一個寄存器變量或者表示一個端口數據就容易出錯,所以說volatile可以保證對特殊地址的穩定訪問。 >>>>注意,在vc6中,一般調試模式沒有進行代碼優化,所以這個關鍵字的作用看不出來。下面通過插入匯編代碼,測試有無volatile關鍵字,對程序最終代碼的影響: >>>>首先,用classwizard建一個win32 console工程,插入一個voltest.cpp文件,輸入下面的代碼:
>>
#i nclude <stdio.h>
void main()
{
int i=10;
int a = i;
printf("i= %d",a);
//下面匯編語句的作用就是改變內存中i的值,但是又不讓編譯器知道 __asm
{
mov dword ptr [ebp-4], 20h
}
int b = i;
printf("i= %d",b);
}
然后,在調試版本模式運行程序,輸出結果如下:
i = 10
i = 32
然后,在release版本模式運行程序,輸出結果如下:
i = 10 i = 10
輸出的結果明顯表明,release模式下,編譯器對代碼進行了優化,第二次沒有輸出正確的i值。下面,我們把 i的聲明加上volatile關鍵字,看看有什么變化:
#i nclude <stdio.h>
void main()
{
volatile int i=10;
int a = i;
printf("i= %d",a);
__asm {
mov dword ptr [ebp-4], 20h
}
int b = i;
printf("i= %d",b);
}
分別在調試版本和release版本運行程序,輸出都是: i = 10 i = 32 這說明這個關鍵字發揮了它的作用!
------------------------------------ volatile對應的變量可能在你的程序本身不知道的情況下發生改變 比如多線程的程序,共同訪問的內存當中,多個程序都可以操縱這個變量 你自己的程序,是無法判定何時這個變量會發生變化 還比如,他和一個外部設備的某個狀態對應,當外部設備發生操作的時候,通過驅動程序和中斷事件,系統改變了這個變量的數值,而你的程序並不知道。 對於volatile類型的變量,系統每次用到他的時候都是直接從對應的內存當中提取,而不會利用cache當中的原有數值,以適應它的未知何時會發生的變化,系統對這種變量的處理不會做優化——
顯然也是因為它的數值隨時都可能變化的情況。 --------------------------------------------------------------------------------
典型的例子
for ( int i=0; i<100000; i++);
這個語句用來測試空循環的速度的,但是編譯器肯定要把它優化掉,根本就不執行 如果你寫成
for ( volatile int i=0; i<100000; i++);
它就會執行了
volatile的本意是“易變的”由於訪問寄存器的速度要快過RAM,所以編譯器一般都會作減少存取外部RAM的優化。比如:
static int i=0;
int main(void)
{ ...
while (1)
{
if (i)
dosomething();
}
}
/* Interrupt service routine. */
void ISR_2(void)
{
i=1;
}
程序的本意是希望ISR_2中斷產生時,在main當中調用dosomething函數,但是,由於編譯器判斷在main函數里面沒有修改過i,因此可能只執行一次對從i到某寄存器的讀操作,然后每次if判斷都只使用這個寄存器里面的“i副本”,導致dosomething永遠也不會被調用。如果將變量加上volatile修飾,則編譯器保證對此變量的讀寫操作都不會被優化(肯定執行)。此例中i也應該如此說明。
一般說來,volatile用在如下的幾個地方:
1、中斷服務程序中修改的供其它程序檢測的變量需要加volatile;
2、多任務環境下各任務間共享的標志應該加volatile;
3、存儲器映射的硬件寄存器通常也要加volatile說明,因為每次對它的讀寫都可能由不同意義;
另外,以上這幾種情況經常還要同時考慮數據的完整性(相互關聯的幾個標志讀了一半被打斷了重寫),在1中可以通過關中斷來實 現,2中可以禁止任務調度,3中則只能依靠硬件的良好設計了。
下面我們來一個個說明。 考慮下面的代碼:
class Gadget
{
public:
void Wait()
{
while (!flag_)
{
Sleep(1000); // sleeps for 1000 milliseconds
}
}
void Wakeup()
{
flag_ = true;
} ...
private:
bool flag_;
};
上面代碼中Gadget::Wait的目的是每過一秒鍾去檢查一下flag_成員變量,當flag_被另一個線程設為true時,該函數才會返回。至少這是程序作者的意圖,然而,這個Wait函數是錯誤的。 假設編譯器發現Sleep(1000)是調用一個外部的庫函數,它不會改變成員變量flag_,那么編譯器就可以斷定它可以把flag_緩存在寄存器中,以后可以訪問該寄存器來代替訪問較慢的主板上的內存。這對於單線程代碼來說是一個很好的優化,但是在現在這種情況下,它破壞了程序的正確性:當你調用了某個Gadget的Wait函數后,即使另一個線程調用了Wakeup,Wait還是會一直循環下去。這是因為flag_的改變沒有反映到緩存它的寄存器中去。編譯器的優化未免有點太……樂觀了。 在大多數情況下,把變量緩存在寄存器中是一個非常有價值的優化方法,如果不用的話很可惜。C和C++給你提供了顯式禁用這種緩存優化的機會。如果你聲明變量是使用了volatile修飾符,編譯器就不會把這個變量緩存在寄存器里——每次訪問都將去存取變量在內存中的實際位置。這樣你要對Gadget的Wait/Wakeup做的修改就是給flag_加上正確的修飾:
class Gadget
{
public: ..
. as above ...
private:
volatile bool flag_;
};
(注:部分代碼來自於Java)
三
(http://zhouruijun163.blog.163.com/blog/static/107715620107274932531/)
順帶記錄下Singleton在多線程中使用的DoubleCheck。
在多線程環境下,使用Singleton模式很重要的一點就是要保證用Double Check機制保證線程安全。 很多時候, 我們通常需要使用singleton模式來保證對象實例的唯一性。通常我們是這么寫的:
class Singleton{
private:
static Singleton *instance;public:
在多線程環境下,使用Singleton模式很重要的一點就是要保證用Double Check機制保證線程安全。
很多時候, 我們通常需要使用singleton模式來保證對象實例的唯一性。通常我們是這么寫的:
class Singleton
{
private:
static Singleton *instance;
public:
static Singleton* getInstance();
private:
Singleton();//將構造函數設為Private以保證只能通過getInstance獲取對象實例.
};
Singleton *Singleton::instance = NULL;
//版本一:
Singleton* Singleton::getInstance()
{
if(NULL == instance) //檢查是否已經生成對象了
{
//對象構造區域[qu yu]
instance = new Singleton();
}
return instance;
}
Singleton::Singleton()
{
//initializing...
}
然而,如果在多線程環境下,Singleton::getInstance() 同時被多個線程調用,也許第一個線程在通過if(NULL == instance)語句后被中斷掛起,這時其它線程也會進入該區域,這時instance = new Singleton();語句就會被調用兩次或者更多,違背了singleton模式的初衷。為了保證對象構造區域為一個互斥區間,這時我們考慮引入mutex互斥信號變量。比如:
//版本2:
Singleton* Singleton::getInstance()
{
lock(mutex);
if(NULL == instance) ////檢查是否已經生成對象了
{
//對象構造區域[qu yu]
instance = new Singleton();
}
release(mutex);
return instance;
}
現在看起來已經足夠安全了,只可能同時有一個線程進入對象構造區域[qu yu]。
此時出現了一個性能問題,每次調用getInstance方法[fang fa]時都需要執行lock(mutex)與release(mutex)的操作,而事實上第一次調用之后,instance就不是NULL值了。
這時候我們就設計一個Double Check的機制:
//版本三:
Singleton* Singleton::getInstance()
{
if(NULL == instance)
{
//對象實例第一次被創建后, 沒有線程會進入該區域了, 因此該版本的性能與版本一幾乎相同,且安全性與版本二一樣好
lock(mutex);
if(NULL == instance) //檢查對象是否已經存在
{
//對象構造區域
instance = new Singleton();
}
release(mutex);
}
return instance;
}
四
總的來說,volatile實際應用中,需要注意的是編譯器優化的陷阱,經驗來自於不斷的實踐,暫時不寫TestProj,繼續啃UE代碼,@todo。。。