C語言中volatile修飾符的作用?


volatile本意為「易變的」。

在嵌入式環境中用volatile關鍵字聲明的變數,在每次對其值進行引用的時候都會從原始地址取值,而不會將值保存在棧或其他位置。

由於該值「易變」的特性所以,針對其的任何賦值或者獲取值操作都會被執行(而不會被優化)。由於這個特性,所以該關鍵字在嵌入式編譯環境中經常用來消除compiler的優化。

舉例說明一些抗優化的1.比如要往某一地址(比如IO口寄存器)送兩指令,讓IO口進行一次翻轉:

uint8_t *addr =...; //設備地址
*addr = 1; //第一個指令
*addr = 0; //第二個指令

以上程序compiler可能做優化而成:

int *addr = ...;
*addr = 0;

結果第一個指令丟失。如果用volatile, compiler就不允許做任何的優化,從而保證程序的原意,MCU庫函數中對於IO寄存器的聲明都會使用volatile。

volatile uint8_t *addr = ...;
*addr = 1;
*addr = 0;

2.用volatile定義的變數會在程序外被改變,每次都必須從內存中讀取,而不能重複使用放在寄存器或棧中的備份。

例如:

volatile uint8_t flag;
flag=0;
while(!flag){
doSomeThing();
}
doSomeThingElse();

如果沒有volatile修飾flag則 doSomeThingElse()可能由於編譯器優化不會被執行(即使flag的值在其他中斷中被更改)。若如上使用了volatile,則會每次都會從原始地址取值,這樣當原始地址的值更改後則while能中止並繼續執行下方代碼。

3.很容易得知多任務環境下各任務間共享的標誌也應該加volatile。(註:並不意味著添加了關鍵字就可以保證多線程安全,非原子操作仍不能保證安全)

4.存儲器映射的硬體寄存器通常也要加voliate,因為每次對它的讀寫都可能有不同意義。

例如:

假設要對一個設備進行初始化,此設備的某一個寄存器為0xXXXXXX。

int *uartSendRegist= (unsigned int *)0xXXXXXXXXX;//定義一個串口數據寄存器地址;
int init(void)
{
int i;
for(i=0;i&< 10;i++) { delaySomeTime(); *uartSendRegist= data[i]; } }

順便說一個問題,一個參數既可以是const還可以是volatile嗎?

這是可以的,因為const的意思是只讀,而不是不變。最簡單的例子比如只讀的狀態寄存器。


Volatile 一般只有寫驅動或者處理信號等才會用到,表示某個內存可能被你程序之外的東西修改。


凡是說在多線程 C/C++ 程序中應該/可以使用 volatile 來修飾共享數據的都是錯的。


volatile不意味著線程安全,不意味著多線程訪問沒有問題

volatile只意味著每次讀和寫操作是有副作用的,並且對他的讀和寫操作一定在下一個序列點前生效,以此來限制編譯器對其的優化。

volatile不代表線程安全,雖然很多編譯器擴展都把volatile變數的多線程訪問做成了線程安全的,但是c標準里可從來沒這麼說過


舉個例子。

這是stm32的3.5版本的固件庫的core_cm3.h中的一段源碼,

/**
* IO definitions
*
* define access restrictions to peripheral registers
*/

#ifdef __cplusplus
#define __I volatile /*!&< defines "read only" permissions */ #else #define __I volatile const /*!&< defines "read only" permissions*/ #endif #define __O volatile /*!&< defines "write only" permissions */ #define __IO volatile /*!&< defines "read / write" permissions*/

可以看出這是用在讀寫外設的許可權控制。比如代碼中出現通過IO口寫一個外設狀態,又讀了回來,可能值已經變化;或連續讀了兩次;又比如使用一個空循環來延時。當開高優化時,編譯器可能改變編程本意,一步到位了。可能會出現一些奇怪的狀況。這時,假如相關變數用__IO修飾就好多了,編譯器不會自作聰明去優化掉。


說一個,實際中我遇到的作用。volatile 可以在多線程訪問同一個變數的時候,阻止編譯器優化掉這個變數。

// volatile make sure not optimized by the compiler
// because two threads modify mainThreadCallback
volatile MainThreadCallback mainThreadCallback;

static void* ThreadRun(void* param)
{
while (true)
{
switch (mainThreadCallback)
{
case main_thread_on_pause:
mainThreadCallback = main_thread_on_wait;
continue;

case main_thread_on_resume:
mainThreadCallback = main_thread_on_null;
break;

case main_thread_on_first_resized:
mainThreadCallback = main_thread_on_null;
break;

case main_thread_on_resized:
mainThreadCallback = main_thread_on_null;
break;
}
}

return NULL;
}

當編譯優化開到 -O3 的時候,mainThreadCallback 不加 volatile 修飾,這個線程就會變成死循環。因為 mainThreadCallback 在主線程會被修改,編譯的時候ThreadRun線程判定 mainThreadCallback 沒有變化所以給優化掉了。


強制訪存


實質意思是用到這個變數的時候,都去這個變數的地址獲取,而不是優化為某個寄存器。

多線程共享變數必須用這個修飾,否則這個變數優化為寄存器就發生bug了。比如while(doing),由另外一個線程設置doing等於0,如果沒有這個修飾,doing初始值為1,編譯器會優化為死循環。

但是不是說通過這個實現多線程變數共享,這個是必要條件,而不是充分條件。多線程變數共享需要原子操作或者信號量。

另外還有常用於嵌入式系統,比如延時,加1計數延時,寄存器變數跟內存變數肯定時間不一樣,加這個修飾這個變數就是內存變數,延時可控。還有用來修飾指針,這個指針指向某個區域,這個區域可能是跟其他外設連接的。比如*p=0,*p=1,*p=0,前面2個賦值在普通內存下是無意義的,而這裡可能用來實現指示燈閃爍,比如1讓指示燈亮,0讓燈滅,這三條語句本來是讓指示燈亮一下,如果編譯器優化成*p=0,顯然就出問題了。


我記得Volatile的作用好像是告訴編譯器不緩存該變數,也就是每次訪問改變數都要去內存訪問,不會訪問該變數在緩存中的副本,也就是CPU的一級二級三級等等緩存,改變的時候也是直接寫回內存的,不會放到緩存里,一個定義為volatile的變數是說這變數可能會被意想不到地改變,這樣,編譯器就不會去假設這個變數的值了。精確地說就是,優化器在用到這個變數時必須每次都小心地重新讀取這個變數的值,而不是使用保存在寄存器里的備份。


1.阻止編譯器為了提高速度將一個變數緩存到寄存器內而不寫回。

2.阻止編譯器調整操作volatile變數的指令順序。

——以上答案來自《程序員的自我修養》29頁。


據我所知的有兩個作用,一是在gcc里插入的彙編碼如果用volatile修飾,則表明這段彙編碼gcc在編譯程序時不得修改;另一個常用的用途是用來修飾變數,表明該變數是經常變化的,不得被緩存,比方說在線程里經常會有while(live){do ...},在這裡live這個變數必須是volatile的。比方說如果因為你頻繁訪問這個變數,結果編譯器優化的時候,直接mov ecx,live;然後cmp ecx,0;jz ...,那你在主線程里怎麼改這個live變數,線程也死不掉


繞過cache,直接讀CPU的寄存器。linux內核用來做原子整型之類的封裝,原因是讀寫這些數據本身只需要一個指令,而加上修身,可以保證讀的時候不會讀取舊的數據。


C++語言的類成員函數,可以用const或者volatile修飾。 const或volatile類對象,只能調用相應的const或者volatile修飾的類成員函數。

這對於使用C++語言編寫並發程序,很有意義。

1。對於可被多個線程訪問的對象,應該聲明為volatile對象。它只能調用volatile修飾的類成員函數,並且這些成員函數內部應該自己實現並發控制,保證數據讀寫的安全(沒有競態條件)。

2。對於已經獲取了互斥鎖或者進入了臨界區的程序,可以獨享對共享數據的讀寫訪問。因此,可以用const_cast運算符把const對象變為普通對象,然後可以訪問一切(包括沒有volatile修飾的)類成員函數。

以上,使得共享對象的並發訪問的安全性得到了編譯器語法級別的保護。


volatile提醒編譯器它後面所定義的變數隨時都有可能改變,因此編譯後的程序每次需要存儲或讀取這個變數的時候,都會直接從變數地址中讀取數據。如果沒有volatile關鍵字,則編譯器可能優化讀取和存儲,可能暫時使用寄存器中的值,如果這個變數由別的程序更新了的話,將出現不一致的現象


編譯器不進行優化

每次取值去內存而非寄存器

變數順序不變


volatile在C code中來修飾變數時,是告訴編譯器該變數的值"易變",對該變數的每一次修改都是有效的,不要對該變數的操作做任何優化,並且每次訪問都從該變數的原始地址訪問。


推薦閱讀:

學習 C/C++ ,有什麼書籍推薦?
C語言中定義int a[10][10],a是什麼類型?
C 字元串常量的空間是不需要回收的?
C++數組名可以看成指針么?
C 語言指針怎麼理解?

TAG:編程 | C編程語言 |