关于volatile关键字的一些总结

volatile


  volatile 关键字的中文释义为 [易变的;易失的],这个关键字用来修饰 C/C++变量,所以变量应该也能体现出易变的特性。我们来看下面两个例子

  第一个,程序中没有用 volatile

#include <stdio.h>

int func(int x){
    return x*x;
}

int main(){
    int a = 5;
    int b = 10;
    int c = 20;
    int d;
  
    a = func(c);
    
    b = a+1;

    printf("%d\n",b);
    d = func(b);

    return 1;
}

第二个程序 ,a用 volatile 来修饰

#include <stdio.h>

int func(int x){
    return x*x;
}

int main(){
    volatile int a = 5;
    int b = 10;
    int c = 20;
    int d;
  
    a = func(c);
    
    b = a+1;

    printf("%d\n",b);
    d = func(b);

    return 1;
}
g++ -S -c -O2 volatile3.cpp -fverbose-asm -o volatile_you.s

生成的汇编代码对比如下
在这里插入图片描述

  左边是没有添加 volatile 的代码,右边是 添加了 volatile 的代码,其实汇编代码没太研究明白,这里留个坑位,但是可以看出左边的代码(没有添加volatile)从89行到98行是没有使用内存地址的,都是寄存器的操作,右边(添加了volatile关键字)的代码从90行 可以看出来,a = func© 的之后把400写入地址 rsp+12(应该是这个意思吧),执行 b = a + 1 的时候,依然要从rsp+12 来读取到寄存器 edx 中,然后 执行

addl	$1, %edx	#, b

  这里能看出一些区别

不可优化性

   对于 非 volatile 的变量,编译器可以把它们全都优化掉,因为编译器通过分析,发现 变量是无用的,就是后面没有用到,就可以进行常量替换,最后的汇编代码就是很简洁,高效率。
  

顺序性

  

下列两段伪代码:

//伪代码
int A;
int B;

void foo(){
	A = B+1;
	B = 0;
}

  一个简单的示例,全局变量A,B均为非volatile变量。通过gcc O2优化进行编译,你可以惊奇的发现,A,B两个变量的赋值顺序被调换了!!!在对应的汇编代码中,B = 0语句先被执行,然后才是A = B + 1语句被执行。

//伪代码
int A;
volatile int B;

void foo(){
	A = B+1;
	B = 0;
}

  变量B被声明为volatile变量。通过查看对应的汇编代码,B仍旧被提前到A之前赋值,Volatile变量B,并未阻止编译器优化的发生,编译后仍旧发生了乱序现象。
  
  
如此看来,C/C++ Volatile变量,与非Volatile变量之间的操作,是可能被编译器交换顺序的。
  
  
那么下段伪代码

//伪代码
volatile int A;
volatile int B;

void foo(){
	A = B+1;
	B = 0;
}

  再来看看对应的汇编。奇迹发生了,A,B赋值乱序的现象消失。此时的汇编代码,与用户代码顺序高度一直,先赋值变量A,然后赋值变量B。如此看来,C/C++ Volatile变量间的操作,是不会被编译器交换顺序的。

多线程 与 volatile

   从我看了几篇博客来说,观点都是认为volatile 其实并不能解决多线程中的一些问题,我看了一些博客,都认为多线程下用 volatile 似乎会产生问题,因此本文遵循看到的博客,结论如下:

  • volatile不能解决多线程中的问题
  • volatile只有是在三种场合下是适合的
    • 和信号处理(signal handler)相关的场合
    • 和内存映射硬件相关的场合
    • 和非本地跳转相关的场合

   在多线程同步中,有一个基本的问题,happens-before。



  
来看这一段伪代码

int something = 0;
volatile bool flag = false;

Thread1(){
	//do something
	something = 1;
	flag = true;
}

Thread2(){
	if(flag == true){
		// assert something happens;
		//实际情况,在假设something已经发生的前提下,做接下来的工作
		assert(something == 1);
		//do other things,depends on sth
		other things;
	}
}

  这段伪代码,声明了一个volatile 的flag 变量,一个线程(Thread1) 在完成一些操作之后,会修改这个变量,而另外一个线程(Thread2),则不断读取这个 flag 变量,由于 flag 被声明为了 volatile 属性,因此编译器在编译时,并不会每次都从寄存器中读取此变量,同时也不会通过各种激进的优化,所以只要flag 变量在 Thread1 中被修改,Thread2中就会读取到这个变化,进入 if 条件判断,然后进入if 内部处理,在if条件的内部,会认为 Thread1 的something 操作一定是完成了,基于这个假设的基础上, 继续进行下面的 other things 的操作。
   因为将flag变量声明为volatile属性,根据上文,按理来说,每次就是从内存中读取,这是一个很好的在多线程中的应用,但是,实际上这样写是有很大的问题存在的,问题的关键是在于:由于 flag = true; ,会假定 Thread1 中给的something已经完成了,但是实际上这里并不能推断出 something 已经完成(由上文的伪代码 A、B 的赋值可以得出此结论)

  
  
   那么如果把所有影响的变量全部改为 volatile 变量,这样是否能阻止编译器优化而导致代码出现的一些问题呢,针对此问题,答案是依然不行,即便将所有的变量都设置为 volatile 变量,首先肯定能阻止编译器的乱序优化,但是我们最终的代码是要运行在 CPU 上的,CPU为了提升代码运行的效率,也会对代码的执行顺序进行调整,目前,市场上存在各种架构的 CPU 产品,也会针对代码进行不同的优化。
      在这里插入图片描述
   从图中可以看到,X86体系(X86,AMD64),也就是我们目前使用最广的CPU,也会存在指令乱序执行的行为:StoreLoad乱序,读操作可以提前到写操作之前进行。

   因此,回到上面的例子,哪怕将所有的变量全部都声明为volatile,哪怕杜绝了编译器的乱序优化,但是针对生成的汇编代码,CPU有可能仍旧会乱序执行指令,导致程序依赖的逻辑出错,volatile对此无能为力。

  
  
  

   针对多线程,真正正确的做法是构建 happens-before 语义,构建这样的语义有很多方法,我们常用的Mutex、Spinlock、RWLock,都能保证这个语义,原子操作也可以。

参考链接
谈谈 C/C++ 中的 volatile
C/C++ volatile关键词深度剖析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值