C++volatile详解

14 篇文章 0 订阅

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

<span style="font-size:14px;">volatile int i=10;
	int a = i;
	...
	// 其他代码,并未明确告诉编译器,对 i 进行过操作
	int b = i;</span>

volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。注意,在 VC 6 中,一般调试模式没有进行代码优化,所以这个关键字的作用看不出来。下面通过插入汇编代码,测试有无 volatile 关键字,对程序最终代码的影响:

输入下面的代码:

<span style="font-size:14px;">	#include <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);
	}</span>

然后,在 Debug 版本模式运行程序,输出结果如下:

i = 10
i = 32

    然后,在 Release 版本模式运行程序,输出结果如下:

i = 10
i = 10

    输出的结果明显表明,Release 模式下,编译器对代码进行了优化,第二次没有输出正确的 i 值。下面,我们把 i 的声明加上 volatile 关键字,看看有什么变化:

<span style="font-size:14px;">	#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);
	}</span>

分别在 Debug 和 Release 版本运行程序,输出都是:

i = 10
i = 32

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

2. 多线程下的volatile?

C/C++多线程编程中不要使用volatile。
(注:这里的意思指的是指望volatile解决多线程竞争问题是有很大风险的,除非所用的环境系统不可靠才会为了保险加上volatile,或者是从极限效率考虑来实现很底层的接口。这要求编写者对程序逻辑走向很清楚才行,不然就会出错)

C++11标准中明确指出解决多线程的数据竞争问题应该使用原子操作或者互斥锁。

C和C++中的volatile并不是用来解决多线程竞争问题的,而是用来修饰一些因为程序不可控因素导致变化的变量,比如访问底层硬件设备的变量,以提醒编译器不要对该变量的访问擅自进行优化。
如果光对共享变量使用volatile修饰而在可能存在竞争的操作中不加锁或使用原子操作对解决多线程竞争没有任何卵用,因为volatile并不能保证操作的原子性,在读取、写入变量的过程中仍然可能被其他线程打断导致意外结果发生。
3. volatile与Double Checked Locking
<span style="font-size:14px;">static int* instance;

int& get_instance()
 {
    if( !instance ) { //检查如果单例的指针是0
        此处有某种锁; //则在此处上锁
	if( !instance ) {  //再判断一次,以防等待锁期间有别的线程已经new完了
 	    instance = new int; //确认无误则new之
	}
    }
    return *instance;
}

int main()
{
    int& i = get_instance();
    i = 111;
    return 1;
}</span>
看看反汇编如何(Intel ICC,O2,为了方便看反汇编禁用inline):
...................
010B1034  mov         eax,dword ptr ds:[010B5100h] //读取instance指针到eax
010B1039  test        eax,eax //检查eax是否为0
010B103B  je          get_instance+12h (010B1042h) //如果为0,则跳转下文010B1042处
...................
010B103D  //此处为下文中跳回的位置
...................
010B1041  ret                 //get_instance()函数返回
................... //010B1042从这里开始
010B1044  call        dword ptr ds:[10B309Ch] //这里面call进去是malloc函数  
010B104A  add         esp,4 //调整栈
010B104D  mov         dword ptr ds:[010B5100h],eax//将malloc出的写回instance地址
010B1052  jmp         get_instance+0Dh (010B103Dh) //跳回前面的代码
.........................
反汇编发现什么问题没? 喂! 判断只做了一次啊!!!! 第二个if去哪里了!
哪里去了? 被编译器优化掉了.... 因为这里的优化逻辑很简单:
如果第一个判断某值==0成功,根本没必要去做第二个判断,因为编译器能发现此值没被这段代码
修改,同时编译器认为此值也不会被其他人"意外"修改,于是,苦心积虑所做的双检锁失效了.跟没写一样.
好了,见证奇迹的时候到了,我们就改一行代码:
<span style="font-size:14px;">static int* volatile instance;</span>
再编译一下,看看反汇编:
01201034  mov         eax,dword ptr ds:[01205100h]  //读取instance指针到eax
01201039  test        eax,eax  //检查eax是否为0
0120103B  je          get_instance+17h (01201047h)//如果为0,则跳转下文01201047h处
.................
01201046  ret  //get_instance()函数返回
.................
//以下为上文中跳转位置01201047:
01201047  mov         eax,dword ptr ds:[01205100h] //再次读取instance指针到eax
0120104C  test        eax,eax  //再次检查eax是否为0
0120104E  jne         get_instance+0Dh (0120103Dh) //如果非0,跳回上文return处
01201050  push        4  //如果还是0,往下执行malloc什么的.
01201052  call        dword ptr ds:[120309Ch] //这里进去是malloc
...........
0120105B  mov         dword ptr ds:[01205100h],eax //将malloc好的值写回instance
01201060  jmp         get_instance+0Dh (0120103Dh) //返回上文
...........
终于,双检锁的逻辑正确了.因为volatile已经提示编译器,instance指针可能被"意外"修改.不要瞎做优化.

这里有一个要吐槽的,intel ICC用最高等级优化,不加volatile的话连第一个判断都被优化掉了,
而MSVC无论怎么开优化,加不加volatile,永远两个判断全做,不愧是安全第一...

特别提醒: 实际上即使加了volatile,这样的双检锁依然不安全,只有原子操作才安全,
4. volatile与const?
const和volatile是否可以一起使用,一起使用是什么意思?
可以同时使用,volatile 作用是避免编译器优化,说明它是随时会变的,它不是 non-const,和const不矛盾。被const修饰的变量只是在当前作用范围无法修改,但是可能被其它程序修改。volatile标识一个变量意味着这个变量可能被非本程序的其他过程改变,例如某个访问这一变量的某中断程序。为变量加上这一标识可以禁止编译器的优化,使程序正确地按设计者的意图运行。

参考:
http://www.cnblogs.com/yc_sunniwell/archive/2010/07/14/1777432.html
w.zhihu.com/question/31459750





  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值