前言

volatile的中文意思是易变的,但这个易变和mutable是不同的含义。mutable是指编译期的易变,根据语法编译器默认不会让我们修改某些变量,但是加上mutable让编译器知道我们要修改的态度很强硬。而volatile的易变是指运行期的易变,这些变化是编译器无法感知的变化,让编译器不要瞎优化。

volatile如何影响编译结果

例一

编译器会将其认为无用的死代码优化掉。

int main()
{
    int* reg = 0x123456; // 假设0x123456是某个特殊寄存器的地址
    *reg = 0x1;
    *reg = 0x2;
    *reg = 0x3;
    *reg = 0x4;
    return 0;
}

上面的代码通过g++ a.cpp -O2编译后,你会发现只有*reg = 0x4;生效了,其他语句被优化掉了。

   0x0000000000400540 <+0>: movl   $0x4,0x123456
   0x000000000040054b <+11>:    xor    %eax,%eax
   0x000000000040054d <+13>:    retq 

一般情况下,这没有什么影响,但是上例中的代码实际是初始化某个设备的状态,任何状态的赋值都不能被省略,否则可能导致设备异常无法工作,此时就需要用到volatile。当加上volatile修饰后,同样通过g++ a.cpp -O2编译后,没有任何语句被优化掉

   0x0000000000400540 <+0>: movl   $0x1,0x123456
   0x000000000040054b <+11>:    xor    %eax,%eax
   0x000000000040054d <+13>:    movl   $0x2,0x123456
   0x0000000000400558 <+24>:    movl   $0x3,0x123456
   0x0000000000400563 <+35>:    movl   $0x4,0x123456
   0x000000000040056e <+46>:    retq 

例二

因为寄存器速度快,所以编译器会使用寄存器达到优化的目的,避免频繁操作内存。
玩过单片机或者ARM板的同学肯定都写过跑马灯程序,下面的程序就算简单模拟一下LED灯忽闪忽闪。

int main()
{
    int* a = (int*)0x123456;
    for( int i = 0; i < 1000; ++i ){
        *a = ~(*a); // 反复取反,开关LED灯
        sleep( 1 );
    }
    return 0;
}

通过g++ a.cpp -O2编译后,你会发现每次取反后的值都保存在寄存器edx中,只在最后把edx里的值保存到了0x123456,这相当于只开关了LED灯一次,不符合预期。

   0x0000000000400540 <+0>: mov    0x123456,%edx
   0x0000000000400547 <+7>: mov    $0x3e8,%eax
   0x000000000040054c <+12>:    nopl   0x0(%rax)
   0x0000000000400550 <+16>:    sub    $0x1,%eax
   0x0000000000400553 <+19>:    not    %edx
   0x0000000000400555 <+21>:    jne    0x400550 <main+16>
   0x0000000000400557 <+23>:    mov    %edx,0x123456
   0x000000000040055e <+30>:    xor    %eax,%eax
   0x0000000000400560 <+32>:    retq 

当加上volatile后,每次取反结果都会存入0x123456,开关LED灯1000次符合预期。

=> 0x0000000000400540 <+0>: mov    $0x3e8,%edx
   0x0000000000400545 <+5>: nopl   (%rax)
   0x0000000000400548 <+8>: mov    0x123456,%eax
   0x000000000040054f <+15>:    sub    $0x1,%edx
   0x0000000000400552 <+18>:    not    %eax
   0x0000000000400554 <+20>:    mov    %eax,0x123456      # 每次取反结果都会存入0x123456
   0x000000000040055b <+27>:    jne    0x400548 <main+8>
   0x000000000040055d <+29>:    xor    %eax,%eax
   0x000000000040055f <+31>:    retq

上面这个例子还可以反一下,即程序统计外部开关LED灯的开关次数(通过中断感知),如果使用寄存器优化的话,CPU可能只感知到一次变化。

例三

编译器可以重排指令方便指令流水化处理,达到优化目的。

int main()
{
    int* a = (int*)0x123456;  // 0x123456是某个设备地址
    *a = 0x1; // 开启该设备
    int* b = (int*)0x654321; // 0x654321是该设备的数据读取地址
    printf( "%d\n", *b ); // 读取该地址的数据
    return 0;;
}

上面代码的逻辑是开启某个设备后,从该设备指定地址读取数据,存在因果关系,但编译器无法识别。因此通过g++ a.cpp -O2编译后,你会发现*b被提前了,程序不符合预期。

=> 0x0000000000400590 <+0>: sub    $0x8,%rsp
   0x0000000000400594 <+4>: mov    0x654321,%esi # 先从0x654321读取数据放入esi作为printf的第二个参数,也就是先执行了*b
   0x000000000040059b <+11>:    movl   $0x1,0x123456 # 后将0x1设置到0x123456
   0x00000000004005a6 <+22>:    mov    $0x400760,%edi # "%d\n"放入edi作为printf的第一个参数
   0x00000000004005ab <+27>:    xor    %eax,%eax
   0x00000000004005ad <+29>:    callq  0x400530 <printf@plt>
   0x00000000004005b2 <+34>:    xor    %eax,%eax
   0x00000000004005b4 <+36>:    add    $0x8,%rsp
   0x00000000004005b8 <+40>:    retq

使用volatile修饰a和b后,指令执行顺序符合预期

=> 0x0000000000400590 <+0>: sub    $0x8,%rsp
   0x0000000000400594 <+4>: movl   $0x1,0x123456 # 先将0x1设置到0x123456
   0x000000000040059f <+15>:    mov    0x654321,%esi # 后从0x654321读取数据放入esi作为printf的第二个参数,也就是先执行了*b
   0x00000000004005a6 <+22>:    mov    $0x400760,%edi
   0x00000000004005ab <+27>:    xor    %eax,%eax
   0x00000000004005ad <+29>:    callq  0x400530 <printf@plt>
   0x00000000004005b2 <+34>:    xor    %eax,%eax
   0x00000000004005b4 <+36>:    add    $0x8,%rsp
   0x00000000004005b8 <+40>:    retq

结语

volatile保证了单一执行线程内,对其修饰的变量的访问不能被优化,以及对另一先序或后序该volatile变量的volatile变量的访问不会被重排序。
从上面的例子能看看出,嵌入式开发中,volatile会比较常用,但在普通应用开发中其实很少用到,一般配合信号处理函数使用。

volatile并不能保证多线程安全。

上述代码运行必崩溃,看看意思就好。
gcc version 4.8.5 20150623

C++系列总结——volatile关键字的更多相关文章

  1. 【面试普通人VS高手系列】volatile关键字有什么用?它的实现原理是什么?

    一个工作了6年的Java程序员,在阿里二面,被问到"volatile"关键字. 然后,就没有然后了- 同样,另外一个去美团面试的工作4年的小伙伴,也被"volatile关 ...

  2. Java多线程干货系列—(四)volatile关键字

    原文地址:http://tengj.top/2016/05/06/threadvolatile4/ <h1 id="前言"><a href="#前言&q ...

  3. 并发系列2:Java并发的基石,volatile关键字、synchronized关键字、乐观锁CAS操作

    由并发大师Doug Lea操刀的并发包Concurrent是并发编程的重要包,而并发包的基石又是volatile关键字.synchronized关键字.乐观锁CAS操作这些基础.因此了解他们的原理对我 ...

  4. 【JUC系列第一篇】-Volatile关键字及内存可见性

    作者:毕来生 微信:878799579 什么是JUC? JUC全称 java.util.concurrent 是在并发编程中很常用的实用工具类 2.Volatile关键字 1.如果一个变量被volat ...

  5. java并发系列(六)-----Java并发:volatile关键字解析

    在 Java 并发编程中,要想使并发程序能够正确地执行,必须要保证三条原则,即:原子性.可见性和有序性.只要有一条原则没有被保证,就有可能会导致程序运行不正确.volatile关键字 被用来保证可见性 ...

  6. Java单例模式和volatile关键字

    单例模式是最简单的设计模式,实现也非常"简单".一直以为我写没有问题,直到被 Coverity 打脸. 1. 暴露问题 前段时间,有段代码被 Coverity 警告了,简化一下代码 ...

  7. 就是要你懂Java中volatile关键字实现原理

    原文地址http://www.cnblogs.com/xrq730/p/7048693.html,转载请注明出处,谢谢 前言 我们知道volatile关键字的作用是保证变量在多线程之间的可见性,它是j ...

  8. Java并发——volatile关键字

    什么是内存可见性? 这里就要提一下JMM(Java内存模型).当线程在运行的时候,并不是直接直接修改电脑主内存中的变量的值.线程间通讯也不是直接把一个线程的变量的值传给另一个线程,让其刷新变量.下面是 ...

  9. Java的 volatile关键字的底层实现原理

    我们知道volatile关键字的作用是保证变量在多线程之间的可见性,它是java.util.concurrent包的核心,没有volatile就没有这么多的并发类给我们使用.本文详细解读一下volat ...

随机推荐

  1. 别以为真懂Openstack: 虚拟机创建的50个步骤和100个知识点(4)

    六.Libvirt 对于Libvirt,在启动虚拟机之前,首先需要define虚拟机,是一个XML格式的文件 列出所有的Instance # virsh list Id    Name         ...

  2. Spring Cloud 微服务架构全链路实践

    阅读目录: 1. 网关请求流程 2. Eureka 服务治理 3. Config 配置中心 4. Hystrix 监控 5. 服务调用链路 6. ELK 日志链路 7. 统一格式返回 Java 微服务 ...

  3. vue项目实践-添加express-mockjs进行数据模拟

    mock-server 在新项目开始的时候,后端框架还没有,前端就有能够自己操作的模拟数据的服务是可以有的 express-mockjs 是楼教主结合 express+mock-lite 造的一个轮子 ...

  4. [Java]LeetCode133. 克隆图 | Clone Graph

    Clone an undirected graph. Each node in the graph contains a label and a list of its neighbors. OJ's ...

  5. [Swift]LeetCode686. 重复叠加字符串匹配 | Repeated String Match

    Given two strings A and B, find the minimum number of times A has to be repeated such that B is a su ...

  6. JVM基础系列第4讲:从源代码到机器码,发生了什么?

    在上篇文章我们聊到,无论什么语言写的代码,其到最后都是通过机器码运行的,无一例外.那么对于 Java 语言来说,其从源代码到机器码,这中间到底发生了什么呢?这就是今天我们要聊的. 如下图所示,编译器可 ...

  7. .NET Core实战项目之CMS 第十一章 开发篇-数据库生成及实体代码生成器开发

    上篇给大家从零开始搭建了一个我们的ASP.NET Core CMS系统的开发框架,具体为什么那样设计我也已经在第十篇文章中进行了说明.不过文章发布后很多人都说了这样的分层不是很合理,什么数据库实体应该 ...

  8. 一张脑图说清 Nginx 的主流程

    一张脑图说清 Nginx 的主流程 这个脑图在 nginx-1.14.0-research 上.这是我在研究nginx的http模块的时候画的.基本上把 Nginx 主流程(特别是 HTTP 的部分) ...

  9. SpringCloud(9)---mysql实现配置中心

    mysql实现配置中心 本公司配置数据的管理是通过mysql进行配置管理,因为已经搭建好了,所以自己动手重新搭建一遍,熟悉整个流程.有关项目源码后期会补上github地址 微服务要实现集中管理微服务配 ...

  10. 『Pushing Boxes 双重bfs』

    Pushing Boxes Description Imagine you are standing inside a two-dimensional maze composed of square ...