C/C++中的volatile
C/C++中的volatile
约定
Volatile 这个话题,涉及到计算机科学多个领域多个层次的诸多细节。仅靠一篇博客,很难穷尽这些细节。因此,若不对讨论范围做一些约定,很容易就有诸多漏洞。到时误人子弟,就不好了。以下是一些基本的约定:
1 这篇博文讨论的volatile关键字,是 C 和 C++ 语言中的关键字。Java 等语言中,也有volatile关键字。但它们和 C/C++ 里的volatile不完全相同,不在这篇博文的讨论范围内。
2 这篇博文讨论的volatile关键字,是限定在 C/C++ 标准之下的。这也就是说,我们讨论的内容应该是与平台无关的,同时也是与编译器扩展无关的。
3 相应的,这篇文章讨论的「标准」指的是 C/C++ 的标准,而不是其他什么东西。
4 我们希望编写的代码是 (1) 符合标准的,(2) 性能良好的,(3) 可移植的。这里 (1) 保证了代码执行结果的正确性,(2) 保证了高效性,(3) 体现了平台无关性(以及编译器扩展等的无关性)。
1 为什么要用volatile?[1]
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改。比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
声明时语法:volatile int vInt;
当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。例如:
volatile int i=10;
int a = i;
int b = i;
volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。
下面通过插入汇编代码,测试有无 volatile 关键字,对程序最终代码的影响:
#include <stdio.h>
void main()
{
int i = 10;
int a = i;
printf("i = %d", a);
// 下面汇编语句的作用就是改变内存中 i 的值
// 但是又不让编译器知道
__asm {
mov dword ptr [ebp-4], 20h
}
int b = i;
printf("i = %d", b);
}
然后,在 Debug 版本模式运行程序,输出结果如下:
i = 10
i = 32
然后,在 Release 版本模式运行程序,输出结果如下:
i = 10
i = 10
输出的结果明显表明,Release 模式下,编译器对代码进行了优化,第二次没有输出正确的 i 值。下面,我们把 i 的声明加上 volatile 关键字,看看有什么变化:
#include <stdio.h>
void main()
{
volatile int i = 10;
int a = i;
printf("i = %d", a);
// 下面汇编语句的作用就是改变内存中 i 的值
// 但是又不让编译器知道
__asm {
mov dword ptr [ebp-4], 20h
}
int b = i;
printf("i = %d", b);
}
分别在 Debug 和 Release 版本运行程序,输出都是:
i = 10
i = 32
这说明这个 volatile 关键字发挥了它的作用。其实不只是“内嵌汇编操纵栈”这种方式属于编译无法识别的变量改变,另外更多的可能是多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程 visible。一般说来,volatile用在如下的几个地方:
1) 中断服务程序中修改的供其它程序检测的变量需要加volatile;
2) 多任务环境下各任务间共享的标志应该加volatile;
3) 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
2 volatile指针
和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念:
- 修饰由指针指向的对象、数据是 const 或 volatile 的:
const char* cpch;
volatile char* vpch;
- 指针自身的值——一个代表地址的整数变量,是 const 或 volatile 的:
char* const pchc;
char* volatile pchv;
注意:(1) 可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。
(2) 除了基本类型外,对用户定义类型也可以用volatile类型进行修饰。
(3) C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,volatile向const一样会从类传递到它的成员。
3 多线程下的volatile
有些变量是用volatile关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用volatile声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值,如下:
volatile BOOL bStop = FALSE;
(1)在一个线程中:
while( !bStop ) { ... }
bStop = FALSE;
return;
(2)在另外一个线程中,要终止上面的线程循环:
bStop = TRUE;
while( bStop );
等待上面的线程终止,如果bStop不使用volatile声明,那么这个循环将是一个死循环,因为bStop已经读取到了寄存器中,寄存器中bStop的值永远不会变成FALSE,加上volatile,程序在执行时,每次均从内存中读出bStop的值,就不会死循环了。
这个关键字是用来设定某个对象的存储位置在内存中,而不是寄存器中。因为一般的对象编译器可能会将其的拷贝放在寄存器中用以加快指令的执行速度,例如下段代码中:
...
int nMyCounter = 0;
for(; nMyCounter<100;nMyCounter++)
{
...
}
...
在此段代码中,nMyCounter的拷贝可能存放到某个寄存器中(循环中,对nMyCounter的测试及操作总是对此寄存器中的值进行),但是另外又有段代码执行了这样的操作:nMyCounter -= 1;这个操作中,对nMyCounter的改变是对内存中的nMyCounter进行操作,于是出现了这样一个现象:nMyCounter的改变不同步。
4 volatile与多线程[2]
volatile可以解决多线程中的某些问题,这一错误认识荼毒多年。例如,在知乎「volatile」话题下的介绍就是「多线程开发中保持可见性的关键字」。为了拨乱反正,这里先给出结论(注意这些结论都基于本文第一节提出的约定之上):
volatile不能解决多线程中的问题。按照 Hans Boehm & Nick Maclaren 的总结,
volatile只在三种场合下是合适的。- 和信号处理(signal handler)相关的场合;
- 和内存映射硬件(memory mapped hardware)相关的场合;
- 和非本地跳转(
setjmp和longjmp)相关的场合。
- 和信号处理(signal handler)相关的场合;
以下我们尝试来用volatile关键字解决多线程同步的一个基本问题:happens-before。
native case
首先我们考虑这样一段(伪)代码。
// global shared data
bool flag = false;
thread1() {
flag = false;
Type* value = new Type(/* parameters */);
thread2(value);
while (true) {
if (flag == true) {
apply(value);
break;
}
}
thread2.join();
if (nullptr != value) { delete value; }
return;
}
thread2(Type* value) {
// do some evaluations
value->update(/* parameters */);
flag = true;
return;
}
这段代码将 thread1 作为主线程,等待 thread2 准备好 value。因此,thread2 在更新 value 之后将 flag 置为真,而 thread1 死循环地检测 flag。简单来说,这段代码的意图希望实现 thread2 在 thread1 使用 value 之前执行完毕这样的语义。
对多线程编程稍有了解的人应该知道,这段代码是有问题的。问题主要出在两个方面。其一,在 thread1 中,flag = false 赋值之后,在 while 死循环里,没有任何机会修改 flag 的值,因此在运行之前,编译器优化可能会将 if (flag == true) 的内容全部优化掉。其二,在 thread2 中,尽管逻辑上 update 需要发生在 flag = true 之前,但编译器和 CPU 并不知道;因此编译器优化和 CPU 乱序执行可能会使 flag = true 发生在 update完成之前,因此 thread1 执行 apply(value) 时可能 value 还未准备好。
加一个 volatile 试试?
在错误的理解中,此时就到了 volatile 登场的时候了。
首先我们考虑这样一段(伪)代码。
// global shared data
volatile bool flag = false; // 1.
thread1() {
flag = false;
Type* value = new Type(/* parameters */);
thread2(value);
while (true) {
if (flag == true) { // 2.
apply(value);
break;
}
}
thread2.join();
if (nullptr != value) { delete value; }
return;
}
thread2(Type* value) {
// do some evaluations
value->update(/* parameters */);
flag = true;
return;
}
这里,在 (1) 处,我们将flag声明为volatile-qualified。因此,在 (2) 处,由于flag == true是对volatile变量的访问,故而if-block 不会被优化消失。然而,尽管flag是volatile-qualified,但value并不是。因此,编译器仍有可能在优化时将thread2中的update和对flag的赋值交换顺序。此外,由于volatile禁止了编译器对flag的优化,这样使用volatile不仅无法达成目的,反而会导致性能下降。
再加一个 volatile 呢?
在错误的理解中,可能会对value也加以volatile关键字修饰;颇有些「没有什么是一个volatile解决不了的;如果不行,那就两个」的意思。
// global shared data
volatile bool flag = false;
thread1() {
flag = false;
volatile Type* value = new Type(/* parameters */); // 1.
thread2(value);
while (true) {
if (flag == true) {
apply(value);
break;
}
}
thread2.join();
if (nullptr != value) { delete value; }
return;
}
thread2(volatile Type* value) {
// do some evaluations
value->update(/* parameters */); // 2.
flag = true;
return;
}
在上一节代码的基础上,(1) 将 value 声明为 volatile-qualified。因此 (2) 处对两个 volatile-qualified 变量进行访问时,编译器不会交换他们的顺序。看起来就万事大吉了。
然而,volatile 只作用在编译器上,但我们的代码最终是要运行在 CPU 上的。尽管编译器不会将 (2) 处换序,但 CPU 的乱序执行(out-of-order execution)已是几十年的老技术了;在 CPU 执行时,value 和 flag 的赋值仍有可能是被换序了的(store-store)。
也许有人会说,x86 和 AMD64 架构的 CPU(大多数个人机器和服务器使用这两种架构的 CPU)只允许 sotre-load 乱序,而不会发生 store-store 乱序;或者在诸如 IA64 架构的处理器上,对
volatile-qualified 变量的访问采用了专门的指令。因而,在这些条件下,这段代码是安全的。尽管如此,使用volatile会禁止编译器优化相关变量,从而降低性能,所以也不建议依赖volatile在这种情况下做线程同步。另一方面,这严重依赖具体的硬件规范,超出了本文的约定讨论范围。
到底应该怎样做?
回顾一下,我们最初遇到的问题其实需要解决两件事情。一是 flag 相关的代码块不能被轻易优化消失,二是要保证线程同步的 happens-before 语义。但本质上,设计使用 flag 本身也就是为了构建 happens-before 语义。这也就是说,两个问题,后者才是核心;如有其他不用 flag 的办法解决问题,那么 flag 就不重要。
对于当前问题,最简单的办法是使用原子操作。
// global shared data
std::atomic<bool> flag = false; // #include <atomic>
thread1() {
flag = false;
Type* value = new Type(/* parameters */);
thread2(value);
while (true) {
if (flag == true) {
apply(value);
break;
}
}
thread2.join();
if (nullptr != value) { delete value; }
return;
}
thread2(Type* value) {
// do some evaluations
value->update(/* parameters */);
flag = true;
return;
}
由于对 std::atomic<bool> 的操作是原子的,同时构建了良好的内存屏障,因此整个代码的行为在标准下是良定义的。
除此之外,还可以结合使用互斥量和条件变量。
// global shared data
std::mutex m; // #include <mutex>
std::condition_variable cv; // #include <condition_variable>
bool flag = false;
thread1() {
flag = false;
Type* value = new Type(/* parameters */);
thread2(value);
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [](){ return flag; });
apply(value);
lk.unlock();
thread2.join();
if (nullptr != value) { delete value; }
return;
}
thread2(Type* value) {
std::lock_guard<std::mutex> lk(m);
// do some evaluations
value->update(/* parameters */);
flag = true;
cv.notify_one();
return;
}
这样一来,由线程之间的同步由互斥量和条件变量来保证,同时也避免了while (true)死循环空耗 CPU 的情况。
C/C++中的volatile的更多相关文章
- [读书笔记]java中的volatile关键词
以下内容大多来自周志明的<深入理解Java虚拟机>. 当一个变量被volatile修饰后,它将具备两种特性: 1. 保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变 ...
- zz剖析为什么在多核多线程程序中要慎用volatile关键字?
[摘要]编译器保证volatile自己的读写有序,但由于optimization和多线程可以和非volatile读写interleave,也就是不原子,也就是没有用.C++11 supposed会支持 ...
- C语言中关键字volatile的含义【转】
本文转载自:http://m.jb51.net/article/37489.htm 本篇文章是对C语言中关键字volatile的含义进行了详细的分析介绍,需要的朋友参考下 volatile 的意思是“ ...
- java多线程中的volatile和synchronized
package com.chzhao; public class Volatiletest extends Thread { private static int count = 0; public ...
- java中的volatile关键字
java中的volatile关键字 一个变量被声明为volatile类型,表示这个变量可能随时被其他线程改变,所以不能把它cache到线程内存(如寄存器)中. 一般情况下volatile不能代替syn ...
- java中的Volatile 变量
Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”:与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少, ...
- C/C++中的volatile简单描述
首先引入一篇博客: 1. 为什么用volatile? C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,通常用于建立语言级别的 memory barrier.这是 BS 在 ...
- C和C++中的volatile、内存屏障和CPU缓存一致性协议MESI
目录 1. 前言2 2. 结论2 3. volatile应用场景3 4. 内存屏障(Memory Barrier)4 5. setjmp和longjmp4 1) 结果1(非优化编译:g++ -g -o ...
- 单例模式中的volatile关键字
在之前学习了单例模式在多线程下的设计,疑惑为何要加volatile关键字.加与不加有什么区别呢?这里我们就来研究一下.单例模式的设计可以参考个人总结的这篇文章 背景:在早期的JVM中,synchr ...
- Java中的volatile关键字的功能
Java中的volatile关键字的功能 volatile是java中的一个类型修饰符.它是被设计用来修饰被不同线程访问和修改的变量.如果不加入volatile,基本上会导致这样的结果:要么无法编写多 ...
随机推荐
- BUGKU_PWN_OVERFLOW2_WP
WP_OVERFLOW2 拿到程序,首先放到我们的kali里面看看是多少位的程序,然后在看看有没有什么安全属性 64位程序,并且开启了RELRO,NX 也就是说,这道题我们需要使用ROP绕过 使用id ...
- Web前端入门第 8 问:HTML <!DOCTYPE> 申明有何用处?如果没有此申明有什么问题?
HELLO,这里是大熊学习前端开发的入门笔记. 本系列笔记基于 windows 系统. 先电脑端浏览器打开任何一个网页,比如百度. 再用 ctrl + u 快捷键即可查看源码,瞅瞅第一行代码,是不是都 ...
- 2D小游戏--猜对应卡牌(unity)
博客地址:https://www.cnblogs.com/zylyehuo/ 项目名称 guess_card_game 参考源码链接: https://www.manning.com/books/un ...
- burp suite使用(一) --- 抓包,截包,改包
接下来我将以一个新手的角度讲述如何使用burp来抓包,截包和改包. 我采用的是UC浏览器来配合burp的使用. 1.设置浏览器 设置---其他---更改代理设置 由于UC浏览器采用的是ie的内核,所以 ...
- 全国省市区基础数据SQL插入脚本
整理了一份全国省市区SQL插入脚本,并配上抓取数据读取插入数据库源码,附件下载地址:https://files.cnblogs.com/files/101Love/Region.rar
- 小白快速了解的Java知识!
Java初学习 1.Java的诞生与崛起 1972年,c语言诞生,其高效率,运行速度快让大批程序员为之倾倒,但是c语言的指针及其内存管理需要程序员自行操作,浪费了大量的时间以及精力,再加上c语言需要尽 ...
- 《机器人SLAM导航核心技术与实战》第1季:第3章_OpenCV图像处理
<机器人SLAM导航核心技术与实战>第1季:第3章_OpenCV图像处理 视频讲解 [第1季]3.第3章_OpenCV图像处理-视频讲解 [第1季]3.1.第3章_OpenCV图像处理_认 ...
- java学习-7-捕获异常处理
Java使用异常来表示错误,并通过try ... catch捕获异常:1.Error错误是你的程序无能为力的,也无法捕获的,比如内存耗尽,最终会由JVM进行捕获打印出信息.几乎听天由命.但是老手程序员 ...
- Git 版本管理,与 SVN区别对比
一.Git vs SVN Git 和 SVN 孰优孰好,每个人有不同的体验. Git是分布式的,SVN是集中式的 这是 Git 和 SVN 最大的区别.若能掌握这个概念,两者区别基本搞懂大半.因为 G ...
- ShardingJdbc学习笔记
Mysql主从复制遇到问题 安装mysql Install/Remove of the Service Denied!错误的解决办法