不要再误解C++ volatile了

李肖遥 2021-08-31 08:30

关注、星标公众号,直达精彩内容

来源:https://liam.page/2018/01/18/volatile-in-C-and-Cpp/

作者:Liam Huang

最近在讨论多线程编程中的一个可能的 false sharing 问题时,有人提出加 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) 体现了平台无关性(以及编译器扩展等的无关性)。


含义


单词 volatile 的含义


在谈及 C/C++ 中的 volatile 关键字时,总有人会拿 volatile 这个英文单词的中文解释说事。他们把 volatile 翻译作「易变的」。但事实上,对于翻译来说,很多时候目标语言很难找到一个词能够反映源语言中单词的全部含义和细节。此处「易变的」就无法做到这一点。


Volatile 的意思,若要详细理解,还是应该查阅权威的英英字典。在柯林斯高阶学习词典中,volatile 是这样解释的:

A situation that is volatile is likely to change suddenly and unexpectedly.

这里对 volatile 的解释有三个精髓的形容词和副词,体现了 volatile 的含义。


1. likely:可能的。这意味着被 volatile 形容的对象「有可能也有可能不」发生改变,因此我们不能对这样的对象的状态做出任何假设。


2. suddenly:突然地。这意味着被 volatile 形容的对象可能发生瞬时改变。


3. unexpectedly:不可预期地。这与 likely 相互呼应,意味着被 volatile 形容的对象可能以各种不可预期的方式和时间发生更改。


因此,volatile 其实就是告诉我们,被它修饰的对象出现任何情况都不要奇怪,我们不能对它们做任何假设。


程序中 volatile 的含义


对于程序员来说,程序本身的任何行为都必须是可预期的。那么,在程序当中,什么才叫 volatile 呢?这个问题的答案也很简单:程序可能受到程序之外的因素影响。


考虑以下 C/C++ 代码。

volatile int *p = /* ... */;int a, b;a = *p;b = *p;

若忽略 volatile,那么 p 就只是一个「指向 int 类型的指针」。这样一来,a = *p; 和 b = *p; 两句,就只需要从内存中读取一次就够了。因为从内存中读取一次之后,CPU 的寄存器中就已经有了这个值;把这个值直接复用就可以了。这样一来,编译器就会做优化,把两次访存的操作优化成一次。这样做是基于一个假设:我们在代码里没有改变 p 指向内存地址的值,那么这个值就一定不会发生改变。

此处说的「读取内存」,包括了读取 CPU 缓存和读取计算机主存。

然而,由于 MMIP(Memory mapped I/O)的存在,这个假设不一定是真的。例如说,假设 p 指向的内存是一个硬件设备。这样一来,从 p 指向的内存读取数据可能伴随着可观测的副作用:硬件状态的修改。此时,代码的原意可能是将硬件设备返回的连续两个 int 分别保存在 a 和 b 当中。这种情况下,编译器的优化就会导致程序行为不符合预期了。


总结来说,被 volatile 修饰的变量,在对其进行读写操作时,会引发一些可观测的副作用。而这些可观测的副作用,是由程序之外的因素决定的。


关键字 volatile 的含义


CPP reference 网站是对 C 和 C++ 语言标准的整理。因此,绝大多数时候,我们可以通过这个网站对语言标准进行查询。关于 volatile 关键字,有 C 语言标准和 C++ 语言标准可查。这里摘录两份标准对 volatile 访问的描述。

C 语言: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.
C++ 语言: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.

这里首先解释两组概念:值类型和序列点(执行序列)。


值类型指的是左值(lvalue)右值(rvalue)这些概念。关于左值和右值,前作有过介绍。简单的理解,左值可以出现在赋值等号的左边,使用时取的是作为对象的身份;右值不可以出现在赋值等号的左边,使用时取的是对象的值。除了 lvalue 和 rvalue,C++ 还定义了其他的值类型。其中,xvalue 大体可以理解为返回右值引用的函数调用或表达式,而 glvalue 则是 lvalue 和 xvalue 之和。


序列点则是 C/C++ 中讨论执行顺序时会提到的概念。对于 C/C++ 的表达式来说,执行表达式有两种类型的动作:(1) 计算某个值、(2) 副作用(例如访问 volatile 对象,原子同步,修改文件等)。因此,如果在两个表达式 E1 和 E2 中间有一个序列点,或者在 C++ 中 E1 于序列中在 E2 之前,则 E1 的求值动作和副作用都会在 E2 的求值动作和副作用之前。关于序列点和序列顺序规则,可以参考:这里和这里。


因此我们讲,在 C/C++ 中,对 volatile 对象的访问,有编译器优化上的副作用:


1. 不允许被优化消失(optimized out);


2. 于序列上在另一个对 volatile 对象的访问之前。


这里提及的「不允许被优化」表示对 volatile 变量的访问,编译器不能做任何假设和推理,都必须按部就班地与「内存」进行交互。因此,上述例中「复用寄存器中的值」就是不允许的。


需要注意的是,无论是 C 还是 C++ 的标准,对于 volatile 访问的序列性,都有单线程执行的前提。其中 C++ 标准特别提及,这个顺序性在多线程环境里不一定成立。


volatile 与多线程


volatile 可以解决多线程中的某些问题,这一错误认识荼毒多年。例如,在知乎「volatile」话题下的介绍就是「多线程开发中保持可见性的关键字」。为了拨乱反正,这里先给出结论(注意这些结论都基于本文第一节提出的约定之上):


1. volatile 不能解决多线程中的问题。


2. 按照 Hans Boehm & Nick Maclaren 的总结,volatile 只在三种场合下是合适的。


2.1 和信号处理(signal handler)相关的场合;


2.2 和内存映射硬件(memory mapped hardware)相关的场合;


2.3 和非本地跳转(setjmp 和 longjmp)相关的场合。


以下我们尝试来用 volatile 关键字解决多线程同步的一个基本问题:happens-before。


首先我们考虑这样一段(伪)代码。

// global shared databool 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 datavolatile 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 datavolatile 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 datastd::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 datastd::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 的情况。

来源整理于网络素材,版权归原作者所有,如有侵权,请联系删除,谢谢。

‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

关注我的微信公众号,回复“加群”按规则加入技术交流群。

欢迎关注我的视频号:


点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看。

李肖遥 公众号“技术让梦想更伟大”,作者:李肖遥,专注嵌入式,只推荐适合你的博文,干货,技术心得,与君共勉。
评论 (0)
  •   无人机蜂群电磁作战仿真系统全解析   一、系统概述   无人机蜂群电磁作战仿真系统是专业的仿真平台,用于模拟无人机蜂群在复杂电磁环境中的作战行为与性能。它构建虚拟电磁环境,模拟无人机蜂群执行任务时可能遇到的电磁干扰与攻击,评估作战效能和抗干扰能力,为其设计、优化及实战应用提供科学依据。   应用案例   目前,已有多个无人机蜂群电磁作战仿真系统在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润无人机蜂群电磁作战仿真系统。这些成功案例为无人机蜂群电磁作战仿真系统的推广和应用提
    华盛恒辉l58ll334744 2025-04-17 16:29 67浏览
  •   北京华盛恒辉无人机电磁兼容模拟训练系统软件是专门用于模拟与分析无人机在复杂电磁环境中电磁兼容性(EMC)表现的软件工具。借助仿真技术,它能帮助用户评估无人机在电磁干扰下的性能,优化电磁兼容设计,保障无人机在复杂电磁环境中稳定运行。   应用案例   目前,已有多个无人机电磁兼容模拟训练系统在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润无人机电磁兼容模拟训练系统。这些成功案例为无人机电磁兼容模拟训练系统的推广和应用提供了有力支持。   系统功能   电磁环境建模:支持三维
    华盛恒辉l58ll334744 2025-04-17 15:10 40浏览
  •   无人机电磁环境效应仿真系统:深度剖析   一、系统概述   无人机电磁环境效应仿真系统,专为无人机在复杂电磁环境下的性能评估及抗干扰能力训练打造。借助高精度仿真技术,它模拟无人机在各类电磁干扰场景中的运行状态,为研发、测试与训练工作提供有力支撑。   应用案例   目前,已有多个无人机电磁环境效应仿真系统在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润无人机电磁环境效应仿真系统。这些成功案例为无人机电磁环境效应仿真系统的推广和应用提供了有力支持。   二、系统功能  
    华盛恒辉l58ll334744 2025-04-17 15:51 58浏览
  • 置信区间反映的是“样本均值”这个统计量的不确定性,因此使用的是标准误(standard error),而不是直接用样本标准差(standard deviation)。标准误体现的是均值的波动程度,而样本标准差体现的是个体数据的波动程度,两者并非一回事,就如下图所显示的一样。下面优思学院会一步一步解释清楚:一、标准差和标准误,究竟差在哪?很多同学对“标准差”和“标准误”这两个概念傻傻分不清楚,但其实差别明显:标准差(Standard Deviation,σ或s):是衡量单个数据点相对于平均值波动的
    优思学院 2025-04-17 13:59 23浏览
  • 一、行业背景与需求智能门锁作为智能家居的核心入口,正从单一安防工具向多场景交互终端演进。随着消费者对便捷性、安全性需求的提升,行业竞争已从基础功能转向成本优化与智能化整合。传统门锁后板方案依赖多颗独立芯片(如MCU、电机驱动、通信模块、语音模块等),导致硬件复杂、功耗高、开发周期长,且成本压力显著。如何通过高集成度方案降低成本、提升功能扩展性,成为厂商破局关键。WTVXXX-32N语音芯片通过“单芯片多任务”设计,将语音播报、电机驱动、通信协议解析、传感器检测等功能整合于一体,为智能门锁后板提供
    广州唯创电子 2025-04-18 09:04 95浏览
  • 【摘要/前言】4月春日花正好,Electronica就在浪漫春日里,盛大启幕。2025年4月15-17日,慕尼黑上海电子展于上海新国际博览中心成功举办。伴随着AI、新能源汽车、半导体的热潮,今年的Electronica盛况空前。请跟随Samtec的视角,感受精彩时刻!【 Samtec展台:老虎的朋友圈技术派对】借天时、占地利、聚人和,Samtec 展台人气爆棚!每年展会与大家相聚,总能收获温暖与动力~Samtec展台位于W3展馆716展位,新老朋友相聚于此,俨然一场线下技术派对!前沿D
    电子资讯报 2025-04-17 11:38 33浏览
  • 现阶段,Zigbee、Z-Wave、Thread、Wi-Fi与蓝牙等多种通信协议在智能家居行业中已得到广泛应用,但协议间互不兼容的通信问题仍在凸显。由于各协议自成体系、彼此割据,智能家居市场被迫催生出大量桥接器、集线器及兼容性软件以在不同生态的设备间构建通信桥梁,而这种现象不仅增加了智能家居厂商的研发成本与时间投入,还严重削减了终端用户的使用体验。为应对智能家居的生态割裂现象,家居厂商需为不同通信协议重复开发适配方案,而消费者则需面对设备入网流程繁琐、跨品牌功能阉割及兼容隐患等现实困境。在此背景
    华普微HOPERF 2025-04-17 17:53 49浏览
  • 一、行业背景与需求随着智能化技术的快速发展和用户对便捷性需求的提升,电动车行业正经历从传统机械控制向智能交互的转型。传统电动车依赖物理钥匙、遥控器和独立防盗装置,存在操作繁琐、功能单一、交互性差等问题。用户期待通过手机等智能终端实现远程控制、实时数据监控及个性化交互体验。为此,将蓝牙语音芯片集成至电动车中控系统,成为推动智能化升级的关键技术路径。二、方案概述本方案通过在电动车中控系统中集成WT2605C蓝牙语音芯片,构建一套低成本、高兼容性的智能交互平台,实现以下核心功能:手机互联控制:支持蓝牙
    广州唯创电子 2025-04-18 08:33 91浏览
  •   无人机蜂群电磁作战仿真系统软件,是专门用于模拟、验证无人机蜂群在电磁作战环境中协同、干扰、通信以及对抗等能力的工具。下面从功能需求、技术架构、典型功能模块、发展趋势及应用场景等方面展开介绍:   应用案例   目前,已有多个无人机蜂群电磁作战仿真系统在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润无人机蜂群电磁作战仿真系统。这些成功案例为无人机蜂群电磁作战仿真系统的推广和应用提供了有力支持。   功能需求   电磁环境建模:模拟构建复杂多样的电磁环境,涵盖各类电磁干扰源与
    华盛恒辉l58ll334744 2025-04-17 16:49 54浏览
  •   无人机电磁兼容模拟训练系统软件:全方位剖析   一、系统概述   北京华盛恒辉无人机电磁兼容模拟训练系统软件,专为满足无人机于复杂电磁环境下的运行需求而打造,是一款专业训练工具。其核心功能是模拟无人机在电磁干扰(EMI)与电磁敏感度(EMS)环境里的运行状况,助力用户评估无人机电磁兼容性能,增强其在复杂电磁场景中的适应水平。   应用案例   目前,已有多个无人机电磁兼容模拟训练系统在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润无人机电磁兼容模拟训练系统。这些成功案例为
    华盛恒辉l58ll334744 2025-04-17 14:52 32浏览
  • 自动驾驶技术的飞速发展,正在重新定义未来出行的边界。从感知到决策,从规划到控制,每一个环节都离不开海量、精准的高质量数据支撑。然而,随着传感器数量的增加和数据规模的指数级增长,行业正面临一系列挑战:多源传感器数据的时间同步难题、复杂数据格式的适配、测量技术的灵活性不足、设备集成周期冗长等,这些问题正成为自动驾驶研发与测试的“隐形瓶颈”。基于技术积累与行业洞察,本文分享一套创新的ADAS时空融合数据采集方案。通过硬件与软件的深度协同优化,能够很好地解决数据采集中的核心痛点,还为自动驾驶研发提供了高
    康谋 2025-04-17 09:54 38浏览
  • 近日,全球6G技术与产业生态大会(简称“全球6G技术大会”)在南京召开。紫光展锐应邀出席“空天地一体化与数字低空”平行论坛,并从6G通信、感知、定位等多方面分享了紫光展锐在6G前沿科技领域的创新理念及在空天地一体化技术方面的研发探索情况。全球6G技术大会是6G领域覆盖广泛、内容全面的国际会议。今年大会以“共筑创新 同享未来”为主题,聚焦6G愿景与关键技术、安全可信、绿色可持续发展等前沿主题,汇聚国内外24家企业、百余名国际知名高校与科研代表共同商讨如何推动全行业6G标准共识形成。6G迈入关键期,
    紫光展锐 2025-04-17 18:55 120浏览
  • 1. 在Ubuntu官网下载Ubuntu server  20.04版本https://releases.ubuntu.com/20.04.6/2. 在vmware下安装Ubuntu3. 改Ubuntu静态IP$ sudo vi /etc/netplan/00-installer-config.yaml# This is the network config written by 'subiquity'network:  renderer: networkd&nbs
    二月半 2025-04-17 16:27 59浏览
我要评论
0
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦