volatile cv

  • const object - an object whose type is const-qualified, or a non-mutable subobject of a const object. Such object cannot be modified: attempt to do so directly is a compile-time error, and attempt to do so indirectly (e.g., by modifying the const object through a reference or pointer to non-const type) results in undefined behavior.
  • volatile object - an object whose type is volatile-qualified, or a subobject of a volatile object, or a mutable subobject of a const-volatile object. Every access (read or write operation, member function call, etc.) made through a glvalue expression of volatile-qualified type is treated as a visible side-effect for the purposes of optimization (that is, within a single thread of execution, volatile accesses cannot be optimized out or reordered with another visible side effect that is sequenced-before or sequenced-after the volatile access. This makes volatile objects suitable for communication with a signal handler, but not with another thread of execution, see std::memory_order). Any attempt to refer to a volatile object through a non-volatile glvalue (e.g. through a reference or pointer to non-volatile type) results in undefined behavior.
  • const volatile object - an object whose type is const-volatile-qualified, a non-mutable subobject of a const volatile object, a const subobject of a volatile object, or a non-mutable volatile subobject of a const object. Behaves as both a const object and as a volatile object.

 

 

https://en.cppreference.com/w/c/language/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;     // 语句1
        b = *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声明为volatile
        int nSum = 0;
     
        nNum = FunA();      // nNum被写入的新内容,其值会缓存在寄存器中
     
        nSum = nNum + 1;    // 此处会从内存(而非寄存器)中读取nNum的值

3 不可优化的

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

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

        volatile int nNum;  // 将nNum声明为volatile
        nNum = 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;    // 语句1
        nNum1 = 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 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 尚未进行赋值为666(语句1尚未执行),所以在判断语句中针对 nNum1 为666的前提下进行的相关操作,就会有问题了。

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

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

说明:由于 Java 语言的 volatile 关键字支持 Acquire、Release 语义,因此 Java 语言的 volatile 能够用来构建 happens-before 语义。也就是说,前面提到的 C/C++ 中 volatile 在多线程下使用出现的问题,在 Java 语言中是不存在的。
————————————————
版权声明:本文为CSDN博主「liitdar」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/liitdar/java/article/details/86063883

posted @ 2020-07-01 14:47  zJanly  阅读(115)  评论(0编辑  收藏  举报