当前位置: 首页 > news >正文

跟我学C++中级篇——volatile的探究

一、volatile的来历

在前面的文章简单分析过volatile,但更倾向于应用。今天就某些开发者在一些不适合的场景下广泛使用volatile进行一下分析说明。volatile本来是从硬件与软件的交互中延伸过来的。大家都学习过缓存,其实这个volatile与这个有点类似。系统为了提高访问速度会把一些变量的值提前读取到寄存器中,比如读取某个硬件的端口数据,编译器可能优化成只去读寄存器中的缓存,导致与设想的不同。这也是嵌入式程序员和普通C或C++程序员的一个较多的区别,看一下代码:

int flag = 0;
bool getSatus(){
while (!flag){work();
}enterOtherWork();return flag;
}

如果没有volatile关键字,这个程序会想当然的进入死循环(从理论分析)。而如果增加volatile关键字,有可能硬件某个时间会向这个循环的控制条件flag内写入停止的条件,所以这个程序一般会在硬件嵌入式的代码中比较容易见到,在上层应用中就非常难了。一般来说,也不会推荐这么写,毕竟有更好的处理方式。
所以说,很多人发现了这个变量的易变性,或者说可见性,即每次都必须去内存中读取相关数据而不是缓存起来应用。就将其引申到了其它的领域,最典型的就是多线程中,但殊不知,危险就已经降临了。很多人之所以没有遇到危险,是因为大多数开发者的场景其实对这个要求并不高。
举一个不恰当的例子,在使用线程的条件变量时,正确的编写方式是应该处理丢失信号和误触发的情况。但其实大多数的应用场景丢失几次信号并没有什么问题,反正在循环等待着,大不了下次一起处理;至于误触发,多循环一次,更无所谓了。明白了吧,不是开发者写的对,是你的环境对这个要求低。

二、volatile的本质

这里强调一下,不同的语言中对volatile处理有细节的不同,特别是从一些高级语言如Java中转过来的一定要注意这个区别。网上的很多volatile都是针对Java语言进行说明的。注意!
回到C++中,volatile的本质其实就以下几个作用:
1、保持变量的可见性
2、限制指令重排(仅针对编译器,对CPU可能无效)
看一下cppreference中的说明:
“Every access (both read and write) made through an lvalue expression of volatile-qualified type is considered an observable side effect for the purpose of optimization and is evaluated strictly according to the rules of the abstract machine (that is, all writes are completed at some time before the next sequence point). This means that within a single thread of execution, a volatile access cannot be optimized out or reordered relative to another visible side effect that is separated by a sequence point from the volatile access.”
说句大白话,volatile的本质就是抵制优化,不管你的优化来自于何方,但有可能力不从心,毕竟不是每个方向上的优化都按标准走或者支持标准。
一般来说不建议在多线程中使用volatile,主要原因在于,此关键字确实解决了多线程间的可见性,但其并未保障其原子性,否则C++新标准中也不会推出std::atomic系列的应用定义(当然,这个内部也用到了volatile,所以说,完成一件事经常是你中有我我中有你)。所以在网上可以看到大家一般的推荐是如果不是在硬件嵌入式的场景下,不建议(甚至激进的杜绝)使用volatile关键字。当然不建议不是指不能用,在一些确定可以使用的场景下,应用也是可以起到非常重要的作用的。这也是在网上可以看到不少的有名开源软件中应用到了这个关键字的原因。前提只有一个,开发者明白自己想要干什么。
volatile关键字的应用场景比较多的有以下几个:
1、硬件嵌入式中的涉及到硬件的操作
2、涉及到系统底层的信号的数据操作
3、多线程之间的状态同步或不敏感数据的操作(基本以基础类型为主,或对原子性无要求等)

而在现在的环境中,大多数属于一种相当复杂的场景,即编译器、指令和内存共同在作用。而指令其实就是CPU的工作,而如今是多核甚至多CPU多核的存在。所以,要想达到类似volatile的目的,不光要考虑编译器的优化,内存的屏障,更要考虑多核以及不同平台的不同指令集的问题。也就是上面提到的内存序的问题。
举一个简单的例子,在X86上由于采用的强一致性内存序,所以volatile大概率没有什么问题的代码可能到了ARM上就不行了,因为ARM不一定使用的是强一致性内存序。所以,如果想靠volatile实现某种功能,有可能会提高性能,但会牺牲代码的可移植性,这也是一个重要的问题。
在前面的相关文章中,也提出过在多核心和未知内存序的情况下要小心应用这个关键字。多线程与volatile无必然关联,还请多注意。

三、具体的例子

下面分析一个单实例创建的例子,基本形式如下:

static A* ins=nullptr;A& GetInstance(){if( nullptr == ins ) {//lock加锁 if( nullptr == ins ) {ins = new A; }}return *ins;
}

其实上面的代码是有问题的,即new的过程是非原子性的,在多线程的情况下,会导致异常的出现。这段代码是不安全的,在编译器优化全开的情况下,可能两层检查就成为了一层检查。这时可以增加volatile(static A* volatile ins=nullptr),发现就不会优化掉一层检查了(有兴趣的可以用不同的编译器用不同的级别优化进行反汇编查看)。但需要注意的是,C++中的volatile与Java中的volatile有所不同,上面的代码即使增加了volatile,仍然是非线程安全的(仍然是原子性的问题),一般现在推荐的是使用std::call_once或者在C++11及以上使用返回局部静态变量,保证单实例在多线程情况下创建的安全性。

四、总结

当然,在不同语言或者不同的语言不同的版本或标准中(比如微软就对C++标准进行了扩展,从VC2005,volatile就支持了固定的内存序,可以安全的在多线程中使用),可能对volatile的应用进行了扩展,那就需要认真的看其的手册及相关技术指南。整体上讲,在没有确切的标准指导下,不建议对volatile进行更宽泛的使用。要严格按照标准的规定来应用,一般不会出什么大的问题。


http://www.mrgr.cn/news/59121.html

相关文章:

  • 多款云存储平台存在安全漏洞,影响超2200万用户
  • 2024年10月-2025年5月 Oracle 19c OCM 考试安排
  • 最新版本jdbcutils集成log4j做详细sql日志、自动释放连接...等
  • OpenCV中的图像通道合并
  • react项目因eslint检测未通过而Failed to compile编译失败
  • ubuntu20.04上使用 Verdaccio 搭建 npm 私有仓库
  • 大厂项目经理推荐的10款常用的项目管理软件值得你收藏
  • Java中TreeSet的使用
  • 代码随想录算法训练营第二十七天|Day27 贪心算法
  • 博图软件的使用(一)
  • 006:看图软件ACDSeePhotoStudio2019安装教程
  • Python中的Bloom Filter算法详解
  • C/C++(七)RAII思想与智能指针
  • Java-图书管理系统
  • 专题十六_栈_队列_优先级队列_算法专题详细总结
  • java核心技术点都有哪些
  • 【C++ 真题】B2099 矩阵交换行
  • 蓝桥杯第二十场小白入门赛
  • 走廊泼水节——求维持最小生成树的完全图的最小边权和
  • 010 操作符详解 上
  • MySQL数据库学习指南
  • RPA技术重塑企业自动化的未来
  • 数据结构之堆的实现以及性质和应用
  • 探寻闲鱼libsgmain加解密算法(3) ——libsgmainso-6.5.XX学习记录
  • 小白学视觉 | PE-YOLO:解决黑夜中的目标检测难点
  • 基于ESP8266的远程推力数据采集系统