溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

C++中volatile關(guān)鍵字如何使用

發(fā)布時(shí)間:2021-07-06 17:34:11 來(lái)源:億速云 閱讀:197 作者:Leah 欄目:編程語(yǔ)言

C++中volatile關(guān)鍵字如何使用,針對(duì)這個(gè)問(wèn)題,這篇文章詳細(xì)介紹了相對(duì)應(yīng)的分析和解答,希望可以幫助更多想解決這個(gè)問(wèn)題的小伙伴找到更簡(jiǎn)單易行的方法。

為什么使用volatile ?

C/C++中的 volatile 關(guān)鍵字 和const對(duì)應(yīng),用來(lái)修飾變量,通常用于建立語(yǔ)言級(jí)別的memory barrier。這是BS在“The C++ Programming Language”對(duì)volatile修飾詞的解釋:A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the language so that aggressive optimizations must be avoided.

volatile 關(guān)鍵字是一種類型修飾符,用它聲明的類型變量表示可以被某些編譯器未知的因素更改,比如:操作系統(tǒng),硬件或者其他線程等。

遇到這個(gè)關(guān)鍵字聲明的變量,編譯器對(duì)訪問(wèn)該變量的代碼就不再進(jìn)行優(yōu)化,從而可以提供對(duì)特殊地址的穩(wěn)定訪問(wèn)。聲明時(shí)語(yǔ)法:int volatile vInt; 當(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 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)。注意,在 VC 6 中,一般調(diào)試模式?jīng)]有進(jìn)行代碼優(yōu)化,所以這個(gè)關(guān)鍵字的作用看不出來(lái)。下面通過(guò)插入?yún)R編代碼,測(cè)試有無(wú) volatile 關(guān)鍵字,對(duì)程序最終代碼的影響:

輸入下面的代碼:

#include <stdio.h>  void main() {  int i = 10;  int a = i;   printf("i = %d", a);   // 下面匯編語(yǔ)句的作用就是改變內(nèi)存中 i 的值  // 但是又不讓編譯器知道  __asm{  mov dword ptr [ebp-4], 20h  }   int b = i;  printf("i = %d", b); } #include <stdio.h> void main(){ int i = 10; int a = i;  printf("i = %d", a);  // 下面匯編語(yǔ)句的作用就是改變內(nèi)存中 i 的值 // 但是又不讓編譯器知道 __asm{ mov dword ptr [ebp-4], 20h }  int b = i; printf("i = %d", b);}

然后,在 Debug 版本模式運(yùn)行程序,輸出結(jié)果如下:

i = 10i = 32然后,在 Release 版本模式運(yùn)行程序,輸出結(jié)果如下:

i = 10i = 10輸出的結(jié)果明顯表明,Release 模式下,編譯器對(duì)代碼進(jìn)行了優(yōu)化,第二次沒(méi)有輸出正確的 i 值。下面,我們把 i 的聲明加上 volatile 關(guān)鍵字,看看有什么變化:

#include <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); } #include <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);}

分別在 Debug 和 Release 版本運(yùn)行程序,輸出都是:

i = 10i = 32

這說(shuō)明這個(gè) volatile 關(guān)鍵字發(fā)揮了它的作用。其實(shí)不只是“內(nèi)嵌匯編操縱?!边@種方式屬于編譯無(wú)法識(shí)別的變量改變,另外更多的可能是多線程并發(fā)訪問(wèn)共享變量時(shí),一個(gè)線程改變了變量的值,怎樣讓改變后的值對(duì)其它線程 visible。一般說(shuō)來(lái),volatile用在如下的幾個(gè)地方: 1) 中斷服務(wù)程序中修改的供其它程序檢測(cè)的變量需要加volatile; 2) 多任務(wù)環(huán)境下各任務(wù)間共享的標(biāo)志應(yīng)該加volatile; 3) 存儲(chǔ)器映射的硬件寄存器通常也要加volatile說(shuō)明,因?yàn)槊看螌?duì)它的讀寫(xiě)都可能由不同意義;

2.volatile 指針

和 const 修飾詞類似,const 有常量指針和指針常量的說(shuō)法,volatile 也有相應(yīng)的概念:

修飾由指針指向的對(duì)象、數(shù)據(jù)是 const 或 volatile 的:

const char* cpch; volatile char* vpch; const char* cpch;volatile char* vpch;

注意:對(duì)于 VC,這個(gè)特性實(shí)現(xiàn)在 VC 8 之后才是安全的。指針自身的值——一個(gè)代表地址的整數(shù)變量,是 const 或 volatile 的:

char*const pchc; char*volatile pchv; char*const pchc;char*volatile pchv;

注意:

(1) 可以把一個(gè)非volatile int賦給volatile int,但是不能把非volatile對(duì)象賦給一個(gè)volatile對(duì)象。

(2) 除了基本類型外,對(duì)用戶定義類型也可以用volatile類型進(jìn)行修飾。(3) C++中一個(gè)有volatile標(biāo)識(shí)符的類只能訪問(wèn)它接口的子集,一個(gè)由類的實(shí)現(xiàn)者控制的子集。用戶只能用const_cast來(lái)獲得對(duì)類型接口的完全訪問(wèn)。此外,volatile向const一樣會(huì)從類傳遞到它的成員。

3. 多線程下的volatile 有些變量是用volatile關(guān)鍵字聲明的。當(dāng)兩個(gè)線程都要用到某一個(gè)變量且該變量的值會(huì)被改變時(shí),應(yīng)該用volatile聲明,該關(guān)鍵字的作用是防止優(yōu)化編譯器把變量從內(nèi)存裝入CPU寄存器中。如果變量被裝入寄存器,那么兩個(gè)線程有可能一個(gè)使用內(nèi)存中的變量,一個(gè)使用寄存器中的變量,這會(huì)造成程序的錯(cuò)誤執(zhí)行。volatile的意思是讓編譯器每次操作該變量時(shí)一定要從內(nèi)存中真正取出,而不是使用已經(jīng)存在寄存器中的值,如下:

volatile BOOL bStop = FALSE; // 在一個(gè)線程中:  while( !bStop ) { ... } bStop = FALSE; return;  //在另外一個(gè)線程中,要終止上面的線程循環(huán): bStop = TRUE; while( bStop ); //等待上面的線程終止, volatile BOOL bStop = FALSE;  // 在一個(gè)線程中:  while( !bStop ) { ... } bStop = FALSE; return;  //在另外一個(gè)線程中,要終止上面的線程循環(huán): bStop = TRUE; while( bStop ); //等待上面的線程終止,

如果bStop不使用volatile申明,那么這個(gè)循環(huán)將是一個(gè)死循環(huán),因?yàn)閎Stop已經(jīng)讀取到了寄存器中,寄存器中bStop的值永遠(yuǎn)不會(huì)變成FALSE,加上volatile,程序在執(zhí)行時(shí),每次均從內(nèi)存中讀出bStop的值,就不會(huì)死循環(huán)了。這個(gè)關(guān)鍵字是用來(lái)設(shè)定某個(gè)對(duì)象的存儲(chǔ)位置在內(nèi)存中,而不是寄存器中。因?yàn)橐话愕膶?duì)象編譯器可能會(huì)將其的拷貝放在寄存器中用以加快指令的執(zhí)行速度,例如下段代碼中:

在此段代碼中,nMyCounter的拷貝可能存放到某個(gè)寄存器中(循環(huán)中,對(duì)nMyCounter的測(cè)試及操作總是對(duì)此寄存器中的值進(jìn)行),但是另外又有段代碼執(zhí)行了這樣的操作:nMyCounter -= 1;這個(gè)操作中,對(duì)nMyCounter的改變是對(duì)內(nèi)存中的nMyCounter進(jìn)行操作,于是出現(xiàn)了這樣一個(gè)現(xiàn)象:nMyCounter的改變不同步。

下面是volatile變量的幾個(gè)例子:

1.并行設(shè)備的硬件寄存器(如:狀態(tài)寄存器

2.一個(gè)中斷服務(wù)子程序中會(huì)訪問(wèn)到的非自動(dòng)變量(Non-automatic variables)

3.多線程應(yīng)用中被幾個(gè)任務(wù)共享的變量

看下面例題:

int square(volatile int *ptr){ return *ptr * *ptr;}

這個(gè)程序有什么問(wèn)題嗎? 如果我們不去關(guān)心volatile關(guān)鍵字的話,那么這個(gè)程序你怎么看都會(huì)覺(jué)得沒(méi)多大問(wèn)題.但是這里

面問(wèn)題大這ne, 首先參數(shù)聲明為volatile就是表明*ptr可能會(huì)隨時(shí)改變.上述代碼運(yùn)行時(shí),編譯器可能產(chǎn)生這樣的代碼:

int square(volatile int *ptr){ int a,b; a = *ptr; b = *ptr; return a * b;}

因?yàn)槟愕?ptr是隨時(shí)都可以意想不到的變化,所以有可能a*b的時(shí)候,a b的值不相同. 這樣你就得到一個(gè)錯(cuò)誤的結(jié)果

改正后的程序:

int square(volatile int *ptr){ int a; a = *ptr; return a * a;}

第二個(gè)問(wèn)題,看如下代碼:

#include<iostream> #include<Windows.h> #include<assert.h>  using namespace std;  int main() {  const int a = 2;  int *p = const_cast<int*>(&a);  *p = 3;  cout << a << endl;  system("pause");  return 0; }

我們有理由的認(rèn)為在內(nèi)存當(dāng)中a的值被修改為3,但是結(jié)果呢? 我們來(lái)看一看

這不科學(xué)啊?? 我們?cè)俅蜷_(kāi)監(jiān)視窗口看一下a的值.

我們都知道監(jiān)視窗口看到的都是從內(nèi)存當(dāng)中拿到的,但是為什么內(nèi)存當(dāng)中為3,打印出來(lái)就是2呢? 我來(lái)解釋一下.

C++編譯器具有優(yōu)化功能,當(dāng)你定一個(gè)const的常量的時(shí)候,系統(tǒng)覺(jué)得它不會(huì)被改變了,于是做一個(gè)優(yōu)化把該常量存到寄

存器當(dāng)中,下次訪問(wèn)的過(guò)程更快速一點(diǎn). 所以當(dāng)顯示窗口讀取數(shù)據(jù)的時(shí)候,他會(huì)直接去寄存器當(dāng)中讀取數(shù)據(jù).而不是去

內(nèi)存,所以導(dǎo)致我們明明該掉了a的值,卻打印不出來(lái).

這個(gè)時(shí)候該我們的volatile出馬了,往i前面加一個(gè)volatile之后就會(huì)解決這個(gè)問(wèn)題,來(lái)看結(jié)果:

談?wù)凜++的volatile關(guān)鍵字以及常見(jiàn)的誤解

近期看到C++標(biāo)準(zhǔn)中對(duì)volatile關(guān)鍵字的定義,發(fā)現(xiàn)和java的volatile關(guān)鍵字完全不一樣,C++的volatile對(duì)并發(fā)編程基本沒(méi)有幫助。網(wǎng)上也看到很多關(guān)于volatile的誤解,于是決定寫(xiě)這篇文章詳細(xì)解釋一下volatile的作用到底是什么。

編譯器對(duì)代碼的優(yōu)化

在講volatile關(guān)鍵字之前,先講一下編譯器的優(yōu)化。

int main() { int i = 0; i++; cout << "hello world" << endl;}

按照代碼,這個(gè)程序會(huì)在內(nèi)存中預(yù)留int大小的空間,初始化這段內(nèi)存為0,然后這段內(nèi)存中的數(shù)據(jù)加1,最后輸出“hello world”到標(biāo)準(zhǔn)輸出中。但是根據(jù)這段代碼編譯出來(lái)的程序(加-O2選項(xiàng)),不會(huì)預(yù)留int大小的內(nèi)存空間,更不會(huì)對(duì)內(nèi)存中的數(shù)字加1。他只會(huì)輸出“hello world”到標(biāo)準(zhǔn)輸出中。

其實(shí)不難理解,這個(gè)是編譯器為了優(yōu)化代碼,修改了程序的邏輯。實(shí)際上C++標(biāo)準(zhǔn)是允許寫(xiě)出來(lái)的代碼和實(shí)際生成的程序不一致的。雖說(shuō)優(yōu)化代碼是件好事情,但是也不能讓編譯器任意修改程序邏輯,不然的話我們沒(méi)辦法寫(xiě)可靠的程序了。所以C++對(duì)這種邏輯的改寫(xiě)是有限制的,這個(gè)限制就是在編譯器修改邏輯后,程序?qū)ν饨绲腎O依舊是不變的。怎么理解呢?實(shí)際上我們可以把我們寫(xiě)出來(lái)的程序看做是一個(gè)黑匣子,如果按照相同的順序輸入相同的輸入,他就每次都會(huì)以同樣的順序給出同樣的輸出。這里的輸入輸出包括了標(biāo)準(zhǔn)輸入輸出、文件系統(tǒng)、網(wǎng)絡(luò)IO、甚至一些system call等等,所有程序外部的事物都包含在內(nèi)。所以對(duì)于程序使用者來(lái)說(shuō),只要兩個(gè)黑匣子的輸入輸出是完全一致的,那么這兩個(gè)黑匣子是一致的,所以編譯器可以在這個(gè)限制下任意改寫(xiě)程序的邏輯。這個(gè)規(guī)則又叫as-if原則。

volatile關(guān)鍵字的作用

不知道有沒(méi)有注意到,剛剛提到輸入輸出的時(shí)候,并沒(méi)有提到內(nèi)存,事實(shí)上,程序?qū)ψ约簝?nèi)存的操作不屬于外部的輸入輸出。這也是為什么在上述例子中,編譯器可以去除對(duì)i變量的操作。但是這又會(huì)出現(xiàn)一個(gè)麻煩,有些時(shí)候操作系統(tǒng)會(huì)把一些硬件映射到內(nèi)存上,讓程序通過(guò)對(duì)內(nèi)存的操作來(lái)操作這個(gè)硬件,比如說(shuō)把磁盤(pán)空間映射到內(nèi)存中。那么對(duì)這部分內(nèi)存的操作實(shí)際上就屬于對(duì)程序外部的輸入輸出了。對(duì)這部分內(nèi)存的操作是不能隨便修改順序的,更不能忽略。這個(gè)時(shí)候volatile就可以派上用場(chǎng)了。按照C++標(biāo)準(zhǔn),對(duì)于glvalue的volatile變量進(jìn)行操作,與其他輸入輸出一樣,順序和內(nèi)容都是不能改變的。這個(gè)結(jié)果就像是把對(duì)volatile的操作看做程序外部的輸入輸出一樣。(glvalue是值類別的一種,簡(jiǎn)單說(shuō)就是內(nèi)存上分配有空間的對(duì)象,更詳細(xì)的請(qǐng)看我的另一篇文章。)

按照C++標(biāo)準(zhǔn),這是volatile唯一的功能,但是在一些編譯器(如,MSVC)中,volatile還有線程同步的功能,但這就是編譯器自己的拓展了,并不能跨平臺(tái)應(yīng)用。

對(duì)volatile常見(jiàn)的誤解

實(shí)際上“volatile可以在線程間同步”也是比較常見(jiàn)的誤解。比如以下的例子:

class AObject{public: void wait() { m_flag = false; while (!m_flag) { this_thread::sleep(1000ms); } } void notify() { m_flag = true; }private: volatile bool m_flag;};AObject obj;...// Thread 1...obj.wait();...// Thread 2...obj.notify();...

對(duì)volatile有誤解的人,或者對(duì)并發(fā)編程不了解的人可能會(huì)覺(jué)得這段邏輯沒(méi)什么問(wèn)題,可能會(huì)認(rèn)為volatile保證了,wait()對(duì)m_flag的讀取,notify()對(duì)m_flag的寫(xiě)入,所以Thread 1能夠正常醒來(lái)。實(shí)際上并不是這么簡(jiǎn)單,因?yàn)樵诙嗪薈PU中,每個(gè)CPU都有自己的緩存。緩存中存有一部分內(nèi)存中的數(shù)據(jù),CPU要對(duì)內(nèi)存讀取與存儲(chǔ)的時(shí)候都會(huì)先去操作緩存,而不會(huì)直接對(duì)內(nèi)存進(jìn)行操作。所以多個(gè)CPU“看到”的內(nèi)存中的數(shù)據(jù)是不一樣的,這個(gè)叫做內(nèi)存可見(jiàn)性問(wèn)題(memory visibility)。放到例子中就是,Thread 2修改了m_flag對(duì)應(yīng)的內(nèi)存,但是Thread 1在其他CPU核上運(yùn)行,所以Thread 1不一定能看到Thread 2對(duì)m_flag做的更改。C++11開(kāi)始,C++標(biāo)準(zhǔn)中有了線程的概念,C++標(biāo)準(zhǔn)規(guī)定了什么情況下一個(gè)線程一定可以看到另一個(gè)線程做的內(nèi)存的修改。而根據(jù)標(biāo)準(zhǔn),上述例子中的Thread 1可能永遠(yuǎn)看不到m_flag變成true,更嚴(yán)重的是,Thread 1對(duì)m_flag的讀取會(huì)導(dǎo)致Undefined Behavior。

從C++標(biāo)準(zhǔn)來(lái)說(shuō),這段代碼是Undefined Behavior,既然是Undefined Behavior的話,是不是也可能正確執(zhí)行?是的,熟悉MESI的應(yīng)該會(huì)知道,Thread 2的修改導(dǎo)致緩存變臟,Thread 1讀取內(nèi)存會(huì)試圖獲取最新的數(shù)據(jù),所以這段代碼可以正常執(zhí)行。那是不是就意味著我們可以放心使用volatile來(lái)做線程的同步?不是的,只是在這個(gè)例子能夠正確執(zhí)行而已。我們對(duì)例子稍作修改,volatile就沒(méi)那么好使了。

class AObject{public: void wait() { m_flag = false; while (!m_flag) { this_thread::sleep(1000ms); } } void notify() { m_flag = true; }private: volatile bool m_flag;};AObject obj;bool something = false;... // Thread 1 ... obj.wait(); assert(something)... // Thread 2 ... something = true;obj.notify(); ...

在以上代碼中,Thread 1的assert語(yǔ)句可能會(huì)失敗。就如前文所說(shuō),C++編譯器在保證as-if原則下可以隨意打亂變量賦值的順序,甚至移除某個(gè)變量。所以上述例子中的“something = true"語(yǔ)句可能發(fā)生在obj.notify()之后。這樣的話,“assert(something)”就會(huì)失敗了。

那么我們可不可能把something也變成volatile?如果something是volatile,我們確實(shí)能夠保證編譯出來(lái)的程序中的語(yǔ)句順序和源代碼一致,但我們?nèi)匀徊荒鼙WC兩個(gè)語(yǔ)句是按照源代碼中的順序執(zhí)行,因?yàn)楝F(xiàn)代CPU往往都有亂序執(zhí)行的功能。所謂亂序執(zhí)行,CPU會(huì)在保證代碼正確執(zhí)行的基礎(chǔ)上,調(diào)整指令的順序,加快程序的運(yùn)算,更多細(xì)節(jié)我們不在這里展開(kāi)。我們?nèi)绻麊慰碩hread 2線程,something和m_flag這兩個(gè)變量的讀寫(xiě)是沒(méi)有依賴關(guān)系的,而Thread 2線程看不到這兩個(gè)變量在其他線程上的依賴關(guān)系,所以CPU可能會(huì)打亂他們的執(zhí)行順序,或者同時(shí)執(zhí)行這兩個(gè)指令。結(jié)果就是,在Thread 1中,obj.wait()返回后,something可能仍然是false,assert失敗。當(dāng)然,會(huì)不會(huì)出現(xiàn)這樣的狀況,實(shí)際上也和具體的CPU有關(guān)系。但是我們知道錯(cuò)誤的代碼可能會(huì)引起錯(cuò)誤的結(jié)果,我們應(yīng)該避免錯(cuò)誤的寫(xiě)法,而這個(gè)錯(cuò)誤就在于誤用了volatile關(guān)鍵字,volatile可以避免優(yōu)化、強(qiáng)制內(nèi)存讀取的順序,但是volatile并沒(méi)有線程同步的語(yǔ)義,C++標(biāo)準(zhǔn)并不能保證它在多線程情況的正確性。

那么用不了volatile,我們?cè)撛趺葱薷纳厦娴睦??C++11開(kāi)始有一個(gè)很好用的庫(kù),那就是atomic類模板,在<atomic>頭文件中,多個(gè)線程對(duì)atomic對(duì)象進(jìn)行訪問(wèn)是安全的,并且提供不同種類的線程同步。不同種類的線程同步非常復(fù)雜,要涉及到C++的內(nèi)存模型與并發(fā)編程,我就不在此展開(kāi)。它默認(rèn)使用的是最強(qiáng)的同步,所以我們就使用默認(rèn)的就好。以下為修改后的代碼:

class AObject{public: void wait() { m_flag = false; while (!m_flag) { this_thread::sleep(1000ms); } } void notify() { m_flag = true; }private: atomic<bool> m_flag;};

只要把“volatile bool”替換為“atomic<bool>”就可以。<atomic>頭文件也定義了若干常用的別名,例如“atomic<bool>”就可以替換為“atomic_bool”。atomic模板重載了常用的運(yùn)算符,所以atomic<bool>使用起來(lái)和普通的bool變量差別不大。

關(guān)于C++中volatile關(guān)鍵字如何使用問(wèn)題的解答就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,如果你還有很多疑惑沒(méi)有解開(kāi),可以關(guān)注億速云行業(yè)資訊頻道了解更多相關(guān)知識(shí)。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI