C++中的volatile

来源:互联网 发布:大数据平台搭建 编辑:程序博客网 时间:2024/05/29 02:14

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 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。例如:

[cpp] view plaincopyprint?
  1. //...  
  2. volatile int i = 10;  
  3. int a = i;  
  4. //...  
  5. // 其他代码,并未明确告诉编译器,对 i 进行过操作  
  6. int b = i;  

volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i 的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。

下面编写程序并通过插入汇编代码,测试有无volatile关键字,对程序最终代码的影响

输入下面的代码:

[cpp] view plaincopyprint?
  1. #include <stdio.h>  
  2.   
  3. void main()  
  4. {  
  5.     int i = 10;  
  6.   
  7.     int a = i;  
  8.   
  9.     printf("i = %d\n", a);  
  10.   
  11.     // 下面汇编语句的作用就是改变内存中 i 的值  
  12.     // 但是又不让编译器知道  
  13.     __asm  
  14.     {  
  15.         MOV DWORD PTR[EBP - 4], 20H  
  16.     }  
  17.   
  18.     int b = i;  
  19.     printf("i = %d\n", b);  
  20. }  
发现上面的程序在Debug,Release模式下的输出都是:


这个程序在Debug,Release模式下的输出一致,说明默认情况下VC++ 10.0在两种模式下都做了代码优化,都造成了编译无法识别变量的改变

再输入下面的代码:

[cpp] view plaincopyprint?
  1. #include <stdio.h>  
  2.   
  3. void main()  
  4. {  
  5.     volatile int i = 10;  
  6.   
  7.     int a = i;  
  8.   
  9.     printf("i = %d\n", a);  
  10.   
  11.     // 下面汇编语句的作用就是改变内存中 i 的值  
  12.     // 但是又不让编译器知道  
  13.     __asm  
  14.     {  
  15.         MOV DWORD PTR[EBP - 4], 20H  
  16.     }  
  17.   
  18.     int b = i;  
  19.     printf("i = %d\n", b);  
  20. }  

发现上面的程序在Debug模式下的输出也是:


但是在Release模式下的输出是:


上面这个程序在不同模式下的输出说明这个 volatile 关键字只在 Release 模式下才发挥了它的作用,在 Debug 模式下并没有达到想要的结果。这也说明,在使用 volatile 修饰符修饰变量时,需要注意程序在 Debug 模式和 Release 模式下的不同行为。


其实不只是“内嵌汇编操纵栈”这种方式属于编译无法识别的变量改变,另外更多的可能是多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程 visible。一般说来,volatile 用在如下的几个地方:
1) 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
2) 多任务环境下各任务间共享的标志应该加 volatile;
3) 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义。

自己的注解:自己在之前的 MCU 程序的编写,以及在多线程程序的编写中,从来没有用到 volatile 修饰符修饰变量。比如在单片机的 C51 程序设计中似乎身边的人都没有用过该修饰符,查阅《单片机的 C 语言应用程序设计》一书也没有讲解关于 volatile 的内容,这不禁让我怀疑是不是 Keil C51 编译器对这一块的默认支持,即 Keil C51 编译器就不会有上面的优化过程,这一点,自己还需要进一步考证。

2.volatile 指针

和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念:

  • 修饰由指针指向的对象、数据是 const 或 volatile 的:

    [cpp] view plaincopyprint?
    1. const char* cpch;  
    2. volatile char* vpch;  

    注意:对于 VC,这个特性实现在 VC 8 之后才是安全的。

  • 指针自身的值——一个代表地址的整数变量,是 const 或 volatile 的:

    [cpp] view plaincopyprint?
    1. charconst pchc;  
    2. charvolatile pchv;  

注意:

(1) 可以把一个非 volatile int 赋给 volatile int,但是不能把非 volatile 对象赋给一个 volatile 对象

(2) 除了基本类型外,对用户定义类型也可以用 volatile 类型进行修饰。

(3) C++ 中一个有 volatile 标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用 const_cast 来获得对类型接口的完全访问。此外,  volatile 向 const 一样会从类传递到它的成员。

3. 多线程下的 volatile

有些变量是用 volatile 关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用 volatile 声明,该关键字的作用是防止优化编译器把变量从内存装入  CPU 寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile 的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值,如下:

[cpp] view plaincopyprint?
  1. volatile BOOL bStop = FALSE;   
(1) 在一个线程中: 
[cpp] view plaincopyprint?
  1. while( !bStop ) { ... }   
  2. bStop = FALSE;   
  3. return;   
(2) 在另外一个线程中,要终止上面的线程循环: 
[cpp] view plaincopyprint?
  1. bStop = TRUE;   
  2. while( bStop ); //等待上面的线程终止  

如果 bStop 不使用 volatile 申明,那么这个循环将是一个死循环,因为 bStop 已经读取到了寄存器中,寄存器中 bStop 的值永远不会变成 FALSE,加上 volatile,程序在执行时,每次均从内存中读出 bStop 的值,就不会死循环了。
这个关键字是用来设定某个对象的存储位置在内存中,而不是寄存器中。因为一般的对象编译器可能会将其的拷贝放在寄存器中用以加快指令的执行速度,例如下段代码中: 

[cpp] view plaincopyprint?
  1. ...   
  2. int nMyCounter = 0;   
  3. for(; nMyCounter<100;nMyCounter++)   
  4. {   
  5. ...   
  6. }   
  7. ...   
在此段代码中,nMyCounter 的拷贝可能存放到某个寄存器中(循环中,对 nMyCounter 的测试及操作总是对此寄存器中的值进行),但是另外又有段代码执行了这样的操作:nMyCounter -= 1;这个操作中,对 nMyCounter 的改变是对内存中的 nMyCounter 进行操作,于是出现了这样一个现象:nMyCounter 的改变不同步。
0 0