转载请保留以下声明
  作者:赵宗晟
  出处:https://www.cnblogs.com/zhao-zongsheng/p/9092520.html

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

编译器对代码的优化

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

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

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

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

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能够正常醒来。实际上并不是这么简单,因为在多核CPU中,每个CPU都有自己的缓存。缓存中存有一部分内存中的数据,CPU要对内存读取与存储的时候都会先去操作缓存,而不会直接对内存进行操作。所以多个CPU“看到”的内存中的数据是不一样的,这个叫做内存可见性问题(memory visibility)。放到例子中就是,Thread 2修改了m_flag对应的内存,但是Thread 1在其他CPU核上运行,所以Thread 1不一定能看到Thread 2对m_flag做的更改。C++11开始,C++标准中有了线程的概念,C++标准规定了什么情况下一个线程一定可以看到另一个线程做的内存的修改。而根据标准,上述例子中的Thread 1可能永远看不到m_flag变成true,更严重的是,Thread 1对m_flag的读取会导致Undefined Behavior。

从C++标准来说,这段代码是Undefined Behavior,既然是Undefined Behavior的话,是不是也可能正确执行?是的,熟悉MESI的应该会知道,Thread 2的修改导致缓存变脏,Thread 1读取内存会试图获取最新的数据,所以这段代码可以正常执行。那是不是就意味着我们可以放心使用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;
bool something = false;
... // Thread 1
...
obj.wait();
assert(something)
... // Thread 2
...
something = true;
obj.notify();
...

在以上代码中,Thread 1的assert语句可能会失败。就如前文所说,C++编译器在保证as-if原则下可以随意打乱变量赋值的顺序,甚至移除某个变量。所以上述例子中的“something = true"语句可能发生在obj.notify()之后。这样的话,“assert(something)”就会失败了。

那么我们可不可能把something也变成volatile?如果something是volatile,我们确实能够保证编译出来的程序中的语句顺序和源代码一致,但我们仍然不能保证两个语句是按照源代码中的顺序执行,因为现代CPU往往都有乱序执行的功能。所谓乱序执行,CPU会在保证代码正确执行的基础上,调整指令的顺序,加快程序的运算,更多细节我们不在这里展开。我们如果单看Thread 2线程,something和m_flag这两个变量的读写是没有依赖关系的,而Thread 2线程看不到这两个变量在其他线程上的依赖关系,所以CPU可能会打乱他们的执行顺序,或者同时执行这两个指令。结果就是,在Thread 1中,obj.wait()返回后,something可能仍然是false,assert失败。当然,会不会出现这样的状况,实际上也和具体的CPU有关系。但是我们知道错误的代码可能会引起错误的结果,我们应该避免错误的写法,而这个错误就在于误用了volatile关键字,volatile可以避免优化、强制内存读取的顺序,但是volatile并没有线程同步的语义,C++标准并不能保证它在多线程情况的正确性。

那么用不了volatile,我们该怎么修改上面的例子?C++11开始有一个很好用的库,那就是atomic类模板,在<atomic>头文件中,多个线程对atomic对象进行访问是安全的,并且提供不同种类的线程同步。不同种类的线程同步非常复杂,要涉及到C++的内存模型与并发编程,我就不在此展开。它默认使用的是最强的同步,所以我们就使用默认的就好。以下为修改后的代码:

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变量差别不大。

谈谈volatile关键字以及常见的误解的更多相关文章

  1. 每日一问:谈谈 volatile 关键字

    这是 wanAndroid 每日一问中的一道题,下面我们来尝试解答一下. 讲讲并发专题 volatile,synchronize,CAS,happens before, lost wake up 为了 ...

  2. JAVA多线程基础学习三:volatile关键字

    Java的volatile关键字在JDK源码中经常出现,但是对它的认识只是停留在共享变量上,今天来谈谈volatile关键字. volatile,从字面上说是易变的.不稳定的,事实上,也确实如此,这个 ...

  3. JAVA多线程学习- 三:volatile关键字

    Java的volatile关键字在JDK源码中经常出现,但是对它的认识只是停留在共享变量上,今天来谈谈volatile关键字. volatile,从字面上说是易变的.不稳定的,事实上,也确实如此,这个 ...

  4. 谈谈以下关键字的作用auto static register const volatile extern

    (1)auto 这个这个关键字用于声明变量的生存期为自动,即将不在任何类.结构.枚举.联合和函数中定义的变量视为全局变量,而在函数中定义的变量视为局部变量.这个关键字不怎么多写,因为所有的变量默认就是 ...

  5. 谈谈对volatile关键字的理解

    1. volatile的特性 volatile是Java语言提供的一种轻量级的同步机制,用来确保将变量得更新操作通知到其它线程.具备三种特性: 保证变量的可见性: 对于volatile修饰的变量进行单 ...

  6. 全面理解Java内存模型(JMM)及volatile关键字(转载)

    关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoad ...

  7. 全面理解Java内存模型(JMM)及volatile关键字

    [版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72772461 出自[zejian ...

  8. 全面理解Java内存模型(JMM)及volatile关键字(转)

    原文地址:全面理解Java内存模型(JMM)及volatile关键字 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型( ...

  9. java架构之路(多线程)JMM和volatile关键字

    说到JMM大家一定很陌生,被我们所熟知的一定是jvm虚拟机,而我们今天讲的JMM和JVM虚拟机没有半毛钱关系,千万不要把JMM的任何事情联想到JVM,把JMM当做一个完全新的事物去理解和认识. 我们先 ...

随机推荐

  1. C语言实现输出一组数字中的所有奇数

    /*第二题*/ #include<stdio.h> //输入186732468 //输出173 //输入12345677 //输出13577 main(){ ;//输入的数字,数字的长度 ...

  2. Niop2017初赛滚粗记

    初赛踢蹬滚粗 TOT (╯°Д°)╯︵┻━┻ ヽ(`Д´)ノ︵ ┻━┻ ┻━┻ 排序啊排序,净是排序,自打我学了C++就再没学过排序!!wtf! (╯°Д°)╯︵ /(.□ . )我tm怎么知道建国那 ...

  3. notepad++中双击选中字符串高亮颜色设置

    notepad++ 中最好用的功能就是双击选中,本文档中所有相同的内容高亮 不过有个问题就是当文档特别大,而且注释比较多的时候,我选中的内容高亮为绿色不太好找,那怎么设置呢? 设置--语言格式设置-- ...

  4. oozie: GC overhead limit exceeded 解决方法

    1.异常表现形式 1)  提示信息      Error java.lang.OutOfMemoryError: GC overhead limit exceeded 2)提示出错      Erro ...

  5. 简单记录numpy库的某些基本功能

    这里介绍python的一个库,numpy库,这个库是机器学习,数据分析最经常用到的库之一,也是利用python做数据必须用到的一个库,入门机器学习学的第一个python库就是它了. 先对其导入到pyt ...

  6. vue 使用踩坑 note

    1. 如图,假如large那一行错写成 'large': item.ext_data.isLarge + '' === 'true',, 那么,编译不报错,控制台无提示,模板不输出. 2. vue的t ...

  7. 第七章 mysql 事务索引以及触发器,视图等等,很重要又难一点点的部分

    [索引] 帮助快速查询 MyISAM ,InnoDB支持btree索引 Memory 支持 btree和hash索引 存储引擎支持 每个表至少16个索引   总索引长度至少256字节   创建索引的优 ...

  8. maven安装和配置及创建maven项目

    (1)下载maven,下载成功后,解压到本地磁盘 里面包含这几项 (2)配置maven环境变量MAVEN_HOME.path (3)最后检验配置是否成功:用win键+R,来打开命令行提示符窗口,即Do ...

  9. unity3d学习路线

    自学游戏开发难不难?小编在这里告诉你:你首先要做的是选择一门开发语言,包括Basic,Pascal,C,C++,等等.也经常会有人争论对于初学者哪门语言更好.对于这一系列流行语言的讨论,我的建议是以C ...

  10. 原生js实现canvas气泡冒泡效果

    说明: 本文章主要分为ES5和ES6两个版本 ES5版本是早期版本,后面用ES6重写优化的,建议使用ES6版本. 1, 原生js实现canvas气泡冒泡效果的插件,api丰富,使用简单2, 只需引入J ...