C/C++编程语言中volatile关键字介绍
本文主要介绍 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关键字介绍相关推荐
- c语言中volatile关键字的作用
读文章之前 可以先看一下<程序员的自我修养 >第28页 过度优化. volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直 ...
- c语言中volatile关键字
volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改. 用volatile关键字声明的变量i每一次被访问时,执行部件都会从i相应的内存单元中取出i的值. 没有用 ...
- 【嵌入式】C语言中volatile关键字
00. 目录 文章目录 00. 目录 01. volatile概述 02. volatile应用场景 03. volatile应用示例 04. 嵌入式系统中应用 05. volatile官方说明 vo ...
- c语言volatile含义,c语言中volatile关键字是什么含义怎么办呢?
满意答案 yyvalentine 2016.11.26 采纳率:56% 等级:11 已帮助:4891人 C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,通常用于建立 ...
- C语言中volatile关键字与汇编__volatile__
在gcc中,可以使用 __asm__ 进行汇编语言的内嵌. __volatile__ 表明编译器不要优化代码,后面的指令保持原样. C语言关键字volatile表明某个变量的值在外部可能被改变,因此对 ...
- C++编程语言中stringstream类介绍
本文主要介绍 C++ 编程语言中 stringstream 类的相关知识,同时通过示例代码介绍 stringstream 类的使用方法. 1 概述 <sstream> 定义了三个类:ist ...
- c语言typeof 变量,c语言中typeof关键字
为什么因为一个关键字而专门写一篇随笔呢?因为这个关键字在Linux里面地位非同一般!这个关键字typeof在linux内核里面使用非常广泛! 下面这两个等效声明,用于声明int类弄的变量a typeo ...
- [Java并发编程(三)] Java volatile 关键字介绍
[Java并发编程(三)] Java volatile 关键字介绍 摘要 Java volatile 关键字是用来标记 Java 变量,并表示变量 "存储于主内存中" .更准确的说 ...
- C语言中extern关键字的使用
C语言中extern关键字的使用,直接上代码. file1.c文件 #include<stdio.h> extern long power(int); int A = 2; int mai ...
- python为什么从0开始_为什么大多数编程语言中,索引都是从0开始
为什么大多数编程语言中,索引都是从0开始 世界上绝大多数语言,但凡涉及到了索引这个概念,索引都是从0开始的,对于初学者,难免觉得别扭,毕竟生活中,但凡涉及到和顺序有关的概念,都是从1开始的,那么为什么 ...
最新文章
- TabHost与RadioGroup结合完成的菜单【带效果图】5个Activity
- groovy --不注意的小错误(java.lang.String.positive() is applicable)
- Can not find the tag library descriptor for http://java.sun.com/jsp/jstl/
- 循环码差错图样matlab,基于MATLAB的(15,7)循环码的编译仿真.doc
- 从键盘中读取字符流 自定义异常
- **Dijkstra算法**
- VIVADO时序约束及STA基础
- office 2010 安装
- 关于MAC的pkg和mpkg的分别
- 锐浪报表加载List泛型数据
- Sqlmap命令讲解
- 计算机显卡驱动异常,解决显卡驱动无法正常安装的问题
- 建立统计回归模型的基本步骤_基本回归模型
- 手把手教你部署Docker(手撸官网)
- C 语言 rand() 和 srand() 使用方法
- 微信小程序实例:实现tabs选项卡效果
- 基于javaweb的律师事务所律师管理系统(java+ssm+html+js+jsp+mysql)
- os.path.abspath() 和 os.path.realpath() 区别
- 舞动黑白方格旗 IBM Power 8重构市场格局
- 基于OHCI的USB主机 —— UFI数据结构3