本文主要介绍 C/C++ 编程语言中 volatile 关键字的相关知识。

1 概述

1.1 Why volatile

volatile 关键字,最早出现于 19 世纪 70 年代,被用于处理 MMIO(Memory-mapped I/O) 带来的问题。

在引入 MMIO 之后,一块内存地址既有可能是真正的内存,也有可能是映射的一个 I/O 端口。因此,读/写一个内存地址,既有可能是真正地操作内存,也有可能是读/写一个 I/O 设备。

那么 MMIO 为什么需要引入 volatile 关键字呢?我们结合下面这段示例代码进行解释:

    unsigned int *p = FunB();unsigned int a;unsigned int b;a = *p;     // 语句1b = *p;     // 语句2*p = a;     // 语句3*p = b;     // 语句4

在上述代码片段中,指针 p 既有可能指向一个内存地址,也有可能指向一个 I/O 设备。如果指针 p 指向的是 I/O 设备,那么语句 1 和语句 2 中的变量 a 和变量 b,就会接收到 I/O 设备的连续两个字节。但是,指针 p 也有可能指向内存地址,这种情况下,编译器就会进行语句优化,编译器的优化策略会判断变量 a 和变量 b 同时从同一个内存地址读取数据,因此在执行完语句 1 之后,直接将变量 a 赋值给变量 b。对于指针 p 指向 I/O 设备的这种情况,就需要防止编译器进行此优化,即不能假设指针 b 指向的内容不变(对应 volatile 关键字的易变性特性)。

同样,语句 3 和语句 4 也有类似的问题,编译器发现将变量 a 和 b 同时赋值给指针 p 是无意义的,因此可能会优化语句 3 中的赋值操作,而仅仅保留语句 4。对于指针 p 指向I/O设备的情况,也需要防止编译器将类似的写操作给优化消失了(对应 volatile 关键字的不可优化特性)。

对于 I/O 设备,编译器不能随意交互指令的顺序,因为指令顺序一变,写入 I/O 设备的内容也就发生变化了(对应 volatile 关键字的顺序性特性)。

为了满足 MMIO 的这三点需求,就有了 volatile 关键字。

1.2 IN C/C++

在 C/C++ 编程语言中,使用 volatile 关键字声明的变量(或对象)通常具有与优化、多线程相关的特殊属性。通常,volatile 关键字用来阻止(伪)编译器对其认为的、无法“被代码本身”改变的代码(变量或对象)进行优化。如在 C/C++ 编程语言中,volatile 关键字可以用来提醒编译器使用 volatile 声明的变量随时有可能改变,因此编译器在代码编译时就不会对该变量进行某些激进的优化,故而编译生成的程序在每次存储或读取该变量时,都会直接从内存地址中读取数据。相反,如果该变量没有使用 volatile 关键字进行声明,则编译器可能会优化读取和存储操作,可能暂时使用寄存器中该变量的值,而如果这个变量由别的程序(线程)更新了的话,就会出现(内存中与寄存器中的)变量值不一致的现象。

在 C/C++ 编程语言中,使用 volatile 关键字声明的变量具有三种特性:易变的不可优化的顺序执行的。下面分别对这三种特性进行介绍。

2 易变的

volatile 在词典中的主要释义就是“易变的”。

在 C/C++ 编程语言中,volatile 的易变性体现在:假设有读、写两条语句,依次对同一个 volatile 变量进行操作,那么后一条的读操作不会直接使用前一条的写操作对应的 volatile 变量的寄存器内容,而是重新从内存中读取该 volatile 变量的值。

上述描述的(部分)示例代码内容如下:

    volatile int nNum = 0;  // 将nNum声明为volatileint nSum = 0;nNum = FunA();      // nNum被写入的新内容,其值会缓存在寄存器中nSum = nNum + 1;    // 此处会从内存(而非寄存器)中读取nNum的值

3 不可优化的

在 C/C++ 编程语言中,volatile 的第二个特性是“不可优化性”。volatile 会告诉编译器,不要对 volatile 声明的变量进行各种激进的优化(甚至将变量直接消除),从而保证程序员写在代码中的指令一定会被执行。

上述描述的(部分)示例代码内容如下:

    volatile int nNum;  // 将nNum声明为volatilenNum = 1;printf("nNum is: %d", nNum);

在上述代码中,如果变量 nNum 没有声明为 volatile 类型,则编译器在编译过程中就会对其进行优化,直接使用常量“1”进行替换(这样优化之后,生成的汇编代码很简介,执行时效率很高)。而当使用 volatile 进行声明后,编译器则不会对其进行优化,nNum 变量仍旧存在,编译器会将该变量从内存中取出,放入寄存器之中,然后再调用 printf() 函数进行打印。

4 顺序执行的

在 C/C++ 编程语言中,volatile 的第三个特性是“顺序执行特性”,即能够保证 volatile 变量间的顺序性不会被编译器进行乱序优化。

C/C++ 编译器最基本优化原理:保证一段程序的输出,在优化前后无变化。

为了对本特性进行深入了解,下面以两个变量(nNum1 和 nNum2)为例(既然存在“顺序执行”,那描述对象必然大于一个),介绍 volatile 的顺序执行特性,示例代码内容如下:

    int nNum1;int nNum2;nNum2 = nNum1 + 1;    // 语句1nNum1 = 10;           // 语句2

在上述代码中:

  • 当 nNum1 和 nNum2 都没有使用 volatile 关键字进行修饰时,编译器会对“语句1”和“语句2”的执行顺序进行优化:即先执行“语句2”、再执行“语句1”;
  • 当 nNum2 使用 volatile 关键字进行修饰时,编译器也可能会对“语句1”和“语句2”的执行顺序进行优化:即先执行“语句2”、再执行“语句1”;
  • 当 nNum1 和 nNum2 都使用 volatile 关键字进行修饰时,编译器不会对“语句1”和“语句2”的执行顺序进行优化:即先执行“语句1”、再执行“语句2”;

说明:上述论述可通过观察代码的生成的汇编代码进行验证。

5 volatile与多线程语义

对于多线程编程而言,在临界区内部,可以通过互斥锁(mutex)保证只有一个线程可以访问该临界区的内容,因此临界区内的变量不需要是 volatile 的;而在临界区外部,被多个线程访问的变量应声明为 volatile 的,这也符合了 volatile 的原意:防止编译器缓存(cache)被多个线程并发用到的变量。

不过,需要注意的是,由于 volatile 关键字的“顺序执行特性”并非会完全保证语句的顺序执行(如 volatile 变量与非 volatile 变量之间的操作;又如一些 CPU 也会对语句的执行顺序进行优化),因此导致了对 volatile 变量的操作并不是原子的,也不能用来为线程建立严格的 happens-before 关系。

对于上述描述,示例代码内容如下:

int nNum1 = 0;
volatile bool flag = false;thread1()
{// some codenNum1 = 666;  // 语句1flag = true;  // 语句2
}thread2()
{// some codeif (true == flag){// 语句3:按照程序设计的预想,此处的nNum1的值应为666,并据此进行逻辑设计}
}

在上述代码中,我们的设计思路是先执行 thread1() 中的“语句1”、“语句2”、再执行 thread2() 中的“语句3”,不过实际上程序的执行结果未必如此。根据前面介绍的 volatile 的“并非会完全保证语句的顺序执行”特性,非 volatile 变量 nNum1 和 volatile 变量 flag 的执行顺序,可能会被编译器(或 CPU)进行乱序优化,最终导致 thread1() 中的“语句2”先于“语句1”执行,当“语句2”执行完成但“语句1”尚未执行时,此时 thread2() 中的判断语句“if (true == flag)”是成立的,但实际上 nNum1 尚未进行赋值操作(语句 1 尚未执行),所以在判断语句中针对“nNum1 == 666”的前提下进行的逻辑设计,就会有问题了。

这是一个在多线程编程中,使用 volatile 不容易发现的问题。

实际上,上述多线程代码想实现的就是一个 happens-before 语义,即保证 thread1() 代码块中的所有代码一定要在 thread2() 代码块的第一条代码之前完成。使用互斥锁(mutex)可以保证 happens-before 语义。但是,在 C/C++ 编程语言中的 volatile 关键字不能保证这个语义,也就意味着在多线程环境下使用 C/C++ 编程语言的 volatile 关键字,如果不够细心,就可能会出现上述问题。所以,使用 C/C++ 编程语言进行多线程编程时,要慎用 volatile 关键字。

说明:由于 Java 编程语言的 volatile 关键字支持 Acquire、Release 语义,因此 Java 语言的 volatile 关键字是能够用来构建 happens-before 语义的。也就是说,前面提到的 C/C++ 编程语言 volatile  关键字在多线程编程中出现的问题,在 Java 编程语言中是不存在的。

C/C++编程语言中volatile关键字介绍相关推荐

  1. c语言中volatile关键字的作用

    读文章之前 可以先看一下<程序员的自我修养 >第28页 过度优化. volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直 ...

  2. c语言中volatile关键字

    volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改. 用volatile关键字声明的变量i每一次被访问时,执行部件都会从i相应的内存单元中取出i的值. 没有用 ...

  3. 【嵌入式】C语言中volatile关键字

    00. 目录 文章目录 00. 目录 01. volatile概述 02. volatile应用场景 03. volatile应用示例 04. 嵌入式系统中应用 05. volatile官方说明 vo ...

  4. c语言volatile含义,c语言中volatile关键字是什么含义怎么办呢?

    满意答案 yyvalentine 2016.11.26 采纳率:56%    等级:11 已帮助:4891人 C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,通常用于建立 ...

  5. C语言中volatile关键字与汇编__volatile__

    在gcc中,可以使用 __asm__ 进行汇编语言的内嵌. __volatile__ 表明编译器不要优化代码,后面的指令保持原样. C语言关键字volatile表明某个变量的值在外部可能被改变,因此对 ...

  6. C++编程语言中stringstream类介绍

    本文主要介绍 C++ 编程语言中 stringstream 类的相关知识,同时通过示例代码介绍 stringstream 类的使用方法. 1 概述 <sstream> 定义了三个类:ist ...

  7. c语言typeof 变量,c语言中typeof关键字

    为什么因为一个关键字而专门写一篇随笔呢?因为这个关键字在Linux里面地位非同一般!这个关键字typeof在linux内核里面使用非常广泛! 下面这两个等效声明,用于声明int类弄的变量a typeo ...

  8. [Java并发编程(三)] Java volatile 关键字介绍

    [Java并发编程(三)] Java volatile 关键字介绍 摘要 Java volatile 关键字是用来标记 Java 变量,并表示变量 "存储于主内存中" .更准确的说 ...

  9. C语言中extern关键字的使用

    C语言中extern关键字的使用,直接上代码. file1.c文件 #include<stdio.h> extern long power(int); int A = 2; int mai ...

  10. python为什么从0开始_为什么大多数编程语言中,索引都是从0开始

    为什么大多数编程语言中,索引都是从0开始 世界上绝大多数语言,但凡涉及到了索引这个概念,索引都是从0开始的,对于初学者,难免觉得别扭,毕竟生活中,但凡涉及到和顺序有关的概念,都是从1开始的,那么为什么 ...

最新文章

  1. TabHost与RadioGroup结合完成的菜单【带效果图】5个Activity
  2. groovy --不注意的小错误(java.lang.String.positive() is applicable)
  3. Can not find the tag library descriptor for http://java.sun.com/jsp/jstl/
  4. 循环码差错图样matlab,基于MATLAB的(15,7)循环码的编译仿真.doc
  5. 从键盘中读取字符流 自定义异常
  6. **Dijkstra算法**
  7. VIVADO时序约束及STA基础
  8. office 2010 安装
  9. 关于MAC的pkg和mpkg的分别
  10. 锐浪报表加载List泛型数据
  11. Sqlmap命令讲解
  12. 计算机显卡驱动异常,解决显卡驱动无法正常安装的问题
  13. 建立统计回归模型的基本步骤_基本回归模型
  14. 手把手教你部署Docker(手撸官网)
  15. C 语言 rand() 和 srand() 使用方法
  16. 微信小程序实例:实现tabs选项卡效果
  17. 基于javaweb的律师事务所律师管理系统(java+ssm+html+js+jsp+mysql)
  18. os.path.abspath() 和 os.path.realpath() 区别
  19. 舞动黑白方格旗 IBM Power 8重构市场格局
  20. 基于OHCI的USB主机 —— UFI数据结构3

热门文章

  1. Postman 把response的值自动放到变量里
  2. 剑指offer第七天
  3. 深入了解Oracle数据字典升级脚本catupgrd.sql调用过程
  4. google搜索url参数总结
  5. ORA-01157、01110问题解决
  6. nginx负载均衡?
  7. Linux系统管理01--系统命令精讲
  8. Java8实战 阅读二周目感想
  9. SecureCRT问题
  10. Linux下完全删除用户