1. 程式人生 > >C/C++中的volatile簡單描述

C/C++中的volatile簡單描述

為什麽 現在 cti space als include pan vpc msdn

首先引入一篇博客:

1. 為什麽用volatile?

C/C++ 中的 volatile 關鍵字和 const 對應,用來修飾變量,通常用於建立語言級別的 memory barrier。這是 BS 在 "The C++ Programming Language" 對 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 關鍵字是一種類型修飾符,用它聲明的類型變量表示可以被某些編譯器未知的因素更改,比如:操作系統、硬件或者其它線程等。遇到這個關鍵字聲明的變量,編譯器對訪問該變量的代碼就不再進行優化,從而可以提供對特殊地址的穩定訪問。聲明時語法:int volatile vInt; 當要求使用 volatile 聲明的變量的值的時候,系統總是重新從它所在的內存讀取數據,即使它前面的指令剛剛從該處讀取過數據。而且讀取的數據立刻被保存。例如:

1 volatile int i=10;
2 int a = i;
3 ...
4 // 其他代碼,並未明確告訴編譯器,對 i 進行過操作
5 int b = i;

volatile 指出 i 是隨時可能發生變化的,每次使用它的時候必須從 i的地址中讀取,因而編譯器生成的匯編代碼會重新從i的地址讀取數據放在 b 中。而優化做法是,由於編譯器發現兩次從 i讀數據的代碼之間的代碼沒有對 i 進行過操作,它會自動把上次讀的數據放在 b 中。而不是重新從 i 裏面讀。這樣以來,如果 i是一個寄存器變量或者表示一個端口數據就容易出錯,所以說 volatile 可以保證對特殊地址的穩定訪問。註意,在 VC 6 中,一般調試模式沒有進行代碼優化,所以這個關鍵字的作用看不出來。下面通過插入匯編代碼,測試有無 volatile 關鍵字,對程序最終代碼的影響:

輸入下面的代碼:

01 #include <stdio.h>
02
03 void main()
04 {
05 int i = 10;
06 int a = i;
07
08 printf("i = %d", a);
09
10 // 下面匯編語句的作用就是改變內存中 i 的值
11 // 但是又不讓編譯器知道
12 __asm {
13 mov dword ptr [ebp-4], 20h
14 }
15
16 int b = i;
17 printf("i = %d", b);
18 }

然後,在 Debug 版本模式運行程序,輸出結果如下:

i = 10
i = 32

然後,在 Release 版本模式運行程序,輸出結果如下:

i = 10
i = 10

輸出的結果明顯表明,Release 模式下,編譯器對代碼進行了優化,第二次沒有輸出正確的 i 值。下面,我們把 i 的聲明加上 volatile 關鍵字,看看有什麽變化:

01 #include <stdio.h>
02
03 void main()
04 {
05 volatile int i = 10;
06 int a = i;
07
08 printf("i = %d", a);
09 __asm {
10 mov dword ptr [ebp-4], 20h
11 }
12
13 int b = i;
14 printf("i = %d", b);
15 }

分別在 Debug 和 Release 版本運行程序,輸出都是:

i = 10
i = 32

這說明這個 volatile 關鍵字發揮了它的作用。其實不只是“內嵌匯編操縱棧”這種方式屬於編譯無法識別的變量改變,另外更多的可能是多線程並發訪問共享變量時,一個線程改變了變量的值,怎樣讓改變後的值對其它線程 visible。一般說來,volatile用在如下的幾個地方:
1) 中斷服務程序中修改的供其它程序檢測的變量需要加volatile;
2) 多任務環境下各任務間共享的標誌應該加volatile;
3) 存儲器映射的硬件寄存器通常也要加volatile說明,因為每次對它的讀寫都可能由不同意義;

2.volatile 指針

和 const 修飾詞類似,const 有常量指針和指針常量的說法,volatile 也有相應的概念:

  • 修飾由指針指向的對象、數據是 const 或 volatile 的:

    1 const char* cpch;
    2 volatile char* vpch;

    註意:對於 VC,這個特性實現在 VC 8 之後才是安全的。

  • 指針自身的值——一個代表地址的整數變量,是 const 或 volatile 的:

    1 char* const pchc;
    2 char* volatile pchv;

註意:(1) 可以把一個非volatile int賦給volatile int,但是不能把非volatile對象賦給一個volatile對象。

   (2) 除了基本類型外,對用戶定義類型也可以用volatile類型進行修飾。
(3) C++中一個有volatile標識符的類只能訪問它接口的子集,一個由類的實現者控制的子集。用戶只能用const_cast來獲得對類型接口的完全訪問。此外,volatile向const一樣會從類傳遞到它的成員。

3. 多線程下的volatile

有些變量是用volatile關鍵字聲明的。當兩個線程都要用到某一個變量且該變量的值會被改變時,應該用volatile聲明,該關鍵字的作用是防止優化編譯器把變量從內存裝入CPU寄存器中。如果變量被裝入寄存器,那麽兩個線程有可能一個使用內存中的變量,一個使用寄存器中的變量,這會造成程序的錯誤執行。volatile的意思是讓編譯器每次操作該變量時一定要從內存中真正取出,而不是使用已經存在寄存器中的值,如下:

volatile BOOL bStop = FALSE;
(1) 在一個線程中:
while( !bStop ) { ... }
bStop = FALSE;
return;
(2) 在另外一個線程中,要終止上面的線程循環:
bStop = TRUE;
while( bStop ); //等待上面的線程終止,如果bStop不使用volatile申明,那麽這個循環將是一個死循環,因為bStop已經讀取到了寄存器中,寄存器中bStop的值永遠不會變成FALSE,加上volatile,程序在執行時,每次均從內存中讀出bStop的值,就不會死循環了。
這個關鍵字是用來設定某個對象的存儲位置在內存中,而不是寄存器中。因為一般的對象編譯器可能會將其的拷貝放在寄存器中用以加快指令的執行速度,例如下段代碼中:
...
int nMyCounter = 0;
for(; nMyCounter<100;nMyCounter++)
{
...
}
...
在此段代碼中,nMyCounter的拷貝可能存放到某個寄存器中(循環中,對nMyCounter的測試及操作總是對此寄存器中的值進行),但是另外又有段代碼執行了這樣的操作:nMyCounter -= 1;這個操作中,對nMyCounter的改變是對內存中的nMyCounter進行操作,於是出現了這樣一個現象:nMyCounter的改變不同步。

上述博客的地址:https://www.cnblogs.com/yc_sunniwell/archive/2010/07/14/1777432.html

我在這裏再次引用一下 The C++ Programming Language:

一種含義的兩種表述形式:

(1) The volatile specifier is used to indicate that an object can be modified by something external to the thread of control.

例如:

  volatile const long clock_register;  // updated by the hardware clock

A volatile specifier basically tells the compiler not to optimize away apparently redundant reads and writes. For example:

auto t1 {clock_register};

// ... not use of clock_register here ...

auto t2{clock_register};

Had clock_register not been volatile, the compiler would have been perfectly entitled to eliminate one of the reads and assume t1 == t2.

(2) A volatile tells the compiler that the value of an object can be changed by something that is not part of the program.

註意事項:

Do not use volatile except in low-level code that deals directly with hardware.

Do not assume that volatile has special meaning in the memory model. It does not. It is not -as in some latter languages - a synchronization mechanism. To get synchronization,

use an atomic, a mutex, or a condition_variable.

關於atomic的進一步說明(摘取自cppreference):

Within a thread of execution, accesses (reads and writes) through volatile glvalues cannot be reordered past observable side-effects (including other volatile accesses) that are sequenced-before or sequenced-after within the same thread, but this order is not guaranteed to be observed by another thread, since volatile access does not establish inter-thread synchronization.

In addition, volatile accesses are not atomic (concurrent read and write is a data race) and do not order memory (non-volatile memory accesses may be freely reordered around the volatile access).

One notable exception is Visual Studio, where, with default settings, every volatile write has release semantics and every volatile read has acquire semantics (MSDN), and thus volatiles may be used for inter-thread synchronization. Standard volatile semantics are not applicable to multithreaded programming, although they are sufficient for e.g. communication with a std::signal handler that runs in the same thread when applied to sig_atomic_t variables.

C/C++中的volatile簡單描述