C++知识篇--volatile关键字

一、volatile简介

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

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

二、volatile关键字声明的变量的特点

2.1 易变的

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

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

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

2.2 不可优化的

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

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

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

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


2.3 顺序执行的

在 C/C++ 编程语言中,volatile 的第三个特性是“顺序执行特性”,即能够保证 volatile 变量间的顺序性不会被编译器进行乱序优化。由于 volatile 关键字的“顺序执行特性”并非会完全保证语句的顺序执行(如 volatile 变量与非 volatile 变量之间的操作;又如一些 CPU 也会对语句的执行顺序进行优化)。

3、volatile与原子性

volatile不保证原子性,它只是保证每次都做真实的数据访问,编译器不去优化它,要实现原子性需要加锁。

volatile不保证原子性,它只是保证每次都做真实的数据访问,编译器不去优化它,要实现原子性需要加锁。

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

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

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

int nNum1 = 0;
volatile bool flag = false;
 
thread1()
{
    // some code
 
    nNum1 = 666;  // 语句1
 
    flag = true;  // 语句2
}
 
thread2()
{
    // some code
 
    if (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 关键字。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值