C++中volatile关键字及常见的误解总结

前言

近期看到C++标准中对volatile关键字的定义,发现和java的volatile关键字完全不一样,C++的volatile对并发编程基本没有帮助。网上也看到很多关于volatile的误解,于是决定写这篇文章详细解释一下volatile的作用到底是什么。

为什么用volatile?

C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,通常用于建立语言级别的 memory barrier。这是 BS 在 "The C++ Programming Language" 对 volatile 修饰词的说明:

A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the language so that aggressive optimizations must be avoided.

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。

编译器对代码的优化

在讲volatile关键字之前,先讲一下编译器的优化。

int main() {
 int i = 0;
 i++;
 cout << "hello world" << endl;
}

按照代码,这个程序会在内存中预留int大小的空间,初始化这段内存为0,然后这段内存中的数据加1,最后输出“hello world”到标准输出中。但是根据这段代码编译出来的程序(加-O2选项),不会预留int大小的内存空间,更不会对内存中的数字加1。他只会输出“hello world”到标准输出中。

其实不难理解,这个是编译器为了优化代码,修改了程序的逻辑。实际上C++标准是允许写出来的代码和实际生成的程序不一致的。虽说优化代码是件好事情,但是也不能让编译器任意修改程序逻辑,不然的话我们没办法写可靠的程序了。所以C++对这种逻辑的改写是有限制的,这个限制就是在编译器修改逻辑后,程序对外界的IO依旧是不变的。怎么理解呢?实际上我们可以把我们写出来的程序看做是一个黑匣子,如果按照相同的顺序输入相同的输入,他就每次都会以同样的顺序给出同样的输出。这里的输入输出包括了标准输入输出、文件系统、网络IO、甚至一些system call等等,所有程序外部的事物都包含在内。所以对于程序使用者来说,只要两个黑匣子的输入输出是完全一致的,那么这两个黑匣子是一致的,所以编译器可以在这个限制下任意改写程序的逻辑。

volatile关键字的作用

不知道有没有注意到,刚刚提到输入输出的时候,并没有提到内存,事实上,程序对自己内存的操作不属于外部的输入输出。这也是为什么在上述例子中,编译器可以去除对i变量的操作。但是这又会出现一个麻烦,有些时候操作系统会把一些硬件映射到内存上,让程序通过对内存的操作来操作这个硬件,比如说把磁盘空间映射到内存中。那么对这部分内存的操作实际上就属于对程序外部的输入输出了。对这部分内存的操作是不能随便修改顺序的,更不能忽略。这个时候volatile就可以派上用场了。按照C++标准,对于glvalue的volatile变量进行操作,与其他输入输出一样,顺序和内容都是不能改变的。这个结果就像是把对volatile的操作看做程序外部的输入输出一样。(glvalue是值类别的一种,简单说就是内存上分配有空间的对象,更详细的请看我的另一篇文章。)

按照C++标准,这是volatile唯一的功能,但是在一些编译器(如,MSVC)中,volatile还有线程同步的功能,但这就是编译器自己的拓展了,并不能跨平台应用。

对volatile常见的误解

实际上“volatile可以在线程间同步”也是比较常见的误解。比如以下的例子:

class AObject
{
public:
 void wait()
 {
 m_flag = false;
 while (!m_flag)
 {
  this_thread::sleep(1000ms);
 }
 }
 void notify()
 {
 m_flag = true;
 }

private:
 volatile bool m_flag;
};

AObject obj;

...

// Thread 1
...
obj.wait();
...

// Thread 2
...
obj.notify();
...

对volatile有误解的人,或者对并发编程不了解的人可能会觉得这段逻辑没什么问题,可能会认为volatile保证了,wait()对m_flag的读取,notify()对m_flag的写入,所以Thread 1能够正常醒来。实际上并不是,Thread 1可能永远看不到m_flag变成true。因为在多核CPU中,每个CPU都有自己的缓存。缓存中存有一部分内存中的数据,CPU要对内存读取与存储的时候都会先去操作缓存,而不会直接对内存进行操作。所以多个CPU“看到”的内存中的数据是不一样的,这个叫做内存可见性问题(memory visibility)。并发编程下,一个程序可以有多个线程在不同的CPU核中同时运行,这个时候内存可见性就会影响程序的正确性。放到例子中就是,Thread 2修改了m_flag对应的内存,但是Thread 1在其他CPU核上运行,而两个CPU缓存和内存没有做同步,导致Thread 1运行的核上看到的一直都是旧的数据,于是Thread 1永远都不能醒来。内存可见性问题不是多线程环境下会遇到的唯一的问题,CPU的乱序执行也会导致一些意想不到的事情发生,关于这点volatile能做的也是有限的。这些都是属于并发编程的内容,在此我就不多做展开,总之volatile关键字对并发编程基本是没有帮助的。

那么用不了volatile,我们该怎么修改上面的例子?C++11开始有一个很好用的库,那就是atomic类模板,在<atomic>头文件中,多个线程对atomic对象进行访问是安全的。以下为修改后的代码:

class AObject
{
public:
 void wait()
 {
 m_flag = false;
 while (!m_flag)
 {
  this_thread::sleep(1000ms);
 }
 }
 void notify()
 {
 m_flag = true;
 }
private:
 atomic<bool> m_flag;
};

只要把“volatile bool”替换为“atomic<bool>”就可以。<atomic>头文件也定义了若干常用的别名,例如“atomic<bool>”就可以替换为“atomic_bool”。atomic模板重载了常用的运算符,所以atomic<bool>使用起来和普通的bool变量差别不大。一些atomic的高级用法,由于要涉及到C++的内存模型与并发编程,我就不在此展开了,以后有时间再补上。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • Java开发中的volatile你必须要了解一下

    前言 上一篇文章说了 CAS 原理,其中说到了 Atomic* 类,他们实现原子操作的机制就依靠了 volatile 的内存可见性特性.如果还不了解 CAS 和 Atomic*,建议看一下我们说的 CAS 自旋锁是什么 并发的三个特性 首先说我们如果要使用 volatile 了,那肯定是在多线程并发的环境下.我们常说的并发场景下有三个重要特性:原子性.可见性.有序性.只有在满足了这三个特性,才能保证并发程序正确执行,否则就会出现各种各样的问题. 原子性,上篇文章说到的 CAS 和 Atomic*

  • Java中Volatile关键字详解及代码示例

    一.基本概念 先补充一下概念:Java内存模型中的可见性.原子性和有序性. 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情.为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的.也就是一个线程修改的结果.另一个线程马上就能看到.比如:用volatile修饰的变量,就会具有可见性.volatile修饰的

  • Java中volatile关键字实现原理

    前言 我们知道volatile关键字的作用是保证变量在多线程之间的可见性,它是java.util.concurrent包的核心,没有volatile就没有这么多的并发类给我们使用. 本文详细解读一下volatile关键字如何保证变量在多线程之间的可见性,在此之前,有必要讲解一下CPU缓存的相关知识,掌握这部分知识一定会让我们更好地理解volatile的原理,从而更好.更正确地地使用volatile关键字. CPU缓存 CPU缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为C

  • Java关键字volatile和synchronized作用和区别

    volatile是变量修饰符,而synchronized则是作用于一段代码或方法:如下三句get代码: int i1; int geti1() {return i1;} volatile int i2; int geti2() {return i2;} int i3; synchronized int geti3() {return i3;} geti1() 得到存储在当前线程中i1的数值.多个线程有多个i1变量拷贝,而且这些i1之间可以相互不同.换句话说,另一个线程可能已经改变了它线程内的i1

  • java同步之volatile解析

    问题 (1)volatile是如何保证可见性的? (2)volatile是如何禁止重排序的? (3)volatile的实现原理? (4)volatile的缺陷? 简介 volatile可以说是Java虚拟机提供的最轻量级的同步机制了,但是它并不容易被正确地理解,以至于很多人不习惯使用它,遇到多线程问题一律使用synchronized或其它锁来解决. 了解volatile的语义对理解多线程的特性具有很重要的意义,所以彤哥专门写了一篇文章来解释volatile的语义到底是什么. 语义一:可见性 前面

  • 简单了解java volatile关键字实现的原理

    一.volatile关键字的语义分析 1.保证可见性 对共享变量的修改,其他线程能够马上感知到.但不能保证原子性(i++) 2.保证有序性 3.volatile的原理和实现机制 有volatile修饰的共享变量进行写操作的时候会多出有 "lock"标志的汇编代码,Lock前缀的指令在多核处理器下会引发两件事情: 1)将当前处理器缓存行中的数据写回到系统内存中 2)这个写回内存的操作会使在其他cpu里缓存了该内存地址的数据无效. 二.volatile的使用场景 1.状态标志(开关模式)

  • Java中的关键字volatile详解

    volatile关键字经常用来修饰变量.不过,volatile本身很容易被误用.本篇就介绍一下volatile的原理和使用方式. 在介绍volatile关键字原理前,我们首先要了解JVM运行时的内存分配逻辑. 对于成员变量i,它存储在堆内存中.每个线程在运行时都会有一个自己的线程栈,线程如果要访问类的成员变量i,会通过引用获取到堆中变量i实际的值10,然后把这个变量值拷贝到自己的栈内存中,作为一个变量副本,之后线程便不再会与堆中的变量有实际联系.每个线程都有一个自己的本地副本,相互隔离.线程访问

  • C++中volatile关键字及常见的误解总结

    前言 近期看到C++标准中对volatile关键字的定义,发现和java的volatile关键字完全不一样,C++的volatile对并发编程基本没有帮助.网上也看到很多关于volatile的误解,于是决定写这篇文章详细解释一下volatile的作用到底是什么. 为什么用volatile? C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,通常用于建立语言级别的 memory barrier.这是 BS 在 "The C++ Programming Language&

  • C++中volatile关键字的使用详解以及常见的误解

    为什么使用volatile ? C/C++中的 volatile 关键字 和const对应,用来修饰变量,通常用于建立语言级别的memory barrier.这是BS在"The C++ Programming Language"对volatile修饰词的解释: A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the lang

  • Java中volatile关键字的作用与用法详解

    volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以重获生机. volatile 关键字作用是,使系统中所有线程对该关键字修饰的变量共享可见,可以禁止线程的工作内存对volatile修饰的变量进行缓存. volatile 2个使用场景: 1.可见性:Java提供了volatile关键字来保证可见性. 当一个共享变量被volatile修饰时,它会保证修

  • 谈谈Java中Volatile关键字的理解

    volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以重获生机.volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情. 一.前言 JMM提供了volatile变量定义.final.synchronized块来保证可见性. 用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值.volatile很容

  • 深入了解Java中Volatile关键字

    一.基本概念 先补充一下概念:Java 内存模型中的可见性.原子性和有序性. 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情.为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的.也就是一个线程修改的结果.另一个线程马上就能看到.比如:用volatile修饰的变量,就会具有可见性.volatile修饰

  • JAVA并发中VOLATILE关键字的神奇之处详解

    并发编程中的三个概念: 1.原子性 在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行. 2.可见性 对于可见性,Java提供了volatile关键字来保证可见性. 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值. 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保

  • java中volatile关键字的作用与实例代码

    一,什么是volatile关键字,作用是什么 volatile是java虚拟机提供的轻量级同步机制 ​ 作用是: 1.保证可见性 2.禁止指令重排 3.不保证原子性 本篇具体就讲解 什么叫保证了可见性, 什么叫禁止指令重排,什么是原子性 而在这之前需要对JMM 有所了解 二,什么是JMM ​ JMM(java 内存模型 Java Memory Model 简称JMM) 本身是一个抽象的概念,并不在内存中真实存在的,它描述的是一组规范或者规则,通过这组规范定义了程序中各个变量(实例字段,静态字段和

  • Java中volatile关键字的作用

    目录 一.volatile作用 二.什么是可见性 三.什么是总线锁和缓存锁 四.什么是指令重排序 一.volatile作用 可以保证多线程环境下共享变量的可见性 通过增加内存屏障防止多个指令之间的重排序 二.什么是可见性 可见性是指当一个线程对于共享变量的修改,其他线程可以立刻看到修改之后的一个值,可见性本质上由几个方面造成的 cpu层面的高速缓存,在cpu里面呢设计了三级缓存去解决cpu运算效率和内存IO效率的问题,但是它也带来缓存一致性的一个问题,而在多线程执行的情况下呢,缓存一致性的问题就

  • Java中Volatile关键字能保证原子性吗

    目录 volatile 有序性 可见性 是否能够保证原子性 volatile volatile 是 Java 中的一个相对来说比较重要的关键字,主要就是用来修饰会被不同线程访问和修改的变量. 而这个变量只能保证两个特性,一个是保证有序性,另外一个则是保证可见性. 那么什么是有序性,什么又是可见性呢? 有序性 那么什么是有序性呢? 其实程序执行的顺序按照代码的先后顺序执行,禁止进行指令重排序. 看似理所当然,其实并不是这样,指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结

随机推荐