不要再误解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  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

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

欢迎关注我的视频号:


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

李肖遥 公众号“技术让梦想更伟大”,作者:李肖遥,专注嵌入式,只推荐适合你的博文,干货,技术心得,与君共勉。
评论
  • 热电偶是zui常用的温度传感器类型。它们用于工业、汽车和消费应用。热电偶是自供电的,可以在很宽的温度范围内工作,并且具有快速的响应时间。热电偶是通过将两条不同的金属线连接在一起制成的。这会导致塞贝克效应。塞贝克效应是两种不同导体的温差在两种物质之间产生电压差的现象。正是这种电压差可以测量并用于计算温度。有几种类型的热电偶由各种不同的材料制成,允许不同的温度范围和不同的灵敏度。不同的类型由zhi定的字母区分。zui常用的是K型。热电偶的一些缺点包括测量温度可能具有挑战性,因为它们的输出电压小,需要
    锦正茂科技 2024-12-05 14:22 25浏览
  • 2024年12月3日至5日,中国电信2024数字科技生态大会在广州举行,通过主题峰会、多场分论坛、重要签约及合作发布等环节,与合作伙伴共绘数字科技发展新愿景。紫光展锐作为中国电信的战略合作伙伴受邀参会,全面呈现了技术、产品创新进展,以及双方在多领域的合作成果。紫光展锐董事长马道杰受邀出席大会主论坛,并在大会期间发表视频致辞。  深化战略合作,共建数字未来马道杰董事长在视频致辞中指出,紫光展锐作为世界一流芯片设计企业,依托在芯片、通信和软硬件集成领域的深厚积累,与中国电信密切合
    紫光展锐 2024-12-05 14:04 35浏览
  • ①辐射发射测试(RE):评估电子、电气产品或系统在工作状态下产生的电磁辐射干扰程度,确保其不会干扰其他电子设备,同时可以确保产品的电磁辐射水平在安全范围内,从而保护用户免受电磁辐射的危害。消费类常见测试标准:EN55032 (RE&CE)、 CLASS A和CLASS B ②传导发射测试(CE):评估电子、电气产品或系统在工作状态下传导电磁骚扰的水平,是确保产品符合电磁兼容性(EMC)要求的重要步骤,保护其他设备免受干扰。常见测试标准:国标18655(RE&CE) 分为5个等级,常规的是过3等级
    时源芯微 2024-12-05 15:16 36浏览
  • 应用环境与客户需求蓝牙设备越来越普及,但在高密度使用环境下,你知道里面潜藏的风险吗?用户在使用蓝牙配件(如键盘、鼠标和耳机)时,经常面临干扰问题,这主要是因为蓝牙设备使用的2.4GHz频段与许多其他无线设备(如Wi-Fi、Thread等)重迭,导致频段拥挤,进而增加干扰的可能性。【常见干扰情境】客服中心:客服中心通常有大量的工作站,每个工作站可能都配备有蓝牙键盘、鼠标和耳机。由于这些设备都使用4GHz频段,客服中心内部的频段拥挤会增加讯号干扰的可能性。再加上中心内部可能有多个无线网络设备和其他电
    百佳泰测试实验室 2024-12-05 16:17 52浏览
  • DT640系列硅二极管温度传感器选用了专门适用于低温温度测量的硅二极管。相比普通硅二极管,具有重复性好、离散性小、精度更高温度范围更宽、低温下电压相对高而易于测量等特点。所有此款温度计都较好地遵循一个电压-温度(V-T)曲线,因而具有更好的可互换性。很多应用中都不需要单独的标定。DT640-BC型裸片温度计,相比市场上的其它温度计,具有尺寸更小、热容更小、响应时间更短的特点。在尺寸、热容以及响应时间有特殊要求的应用中具有du特的优势。   以下是二极管温度传感器的测
    锦正茂科技 2024-12-05 13:57 23浏览
  • RK3506单板机(卡片电脑)是一款高性能三核Cortex-A7处理器,内部集成Cortex-M0核心,RK3506单板机具有接口丰富、实时性高、显示开发简单、低功耗及多系统支持等特点,非常适合于工业控制、工业通信、人机交互等应用场景。 多核异构3xCortex-A7+Cortex-M0 外设接口丰富,板载网络、串口、CAN总线 支持Buildroot、Yocto系统,支持AMP混合部署 支持2D硬件加速,适用于轻量级HMI目前RK3506主要分为3种型号
    万象奥科 2024-12-05 16:59 52浏览
  • 在阅读了《高速PCB设计经验规则应用实践》后,对于PCB设计的布局经验有了更为深入和系统的理解。该书不仅详细阐述了高速PCB设计中的经验法则,还通过实际案例和理论分析,让读者能够更好地掌握这些法则并将其应用于实际工作中。布局是走线的基础,预先的规划再到叠层的选择,电源和地的分配,信号网络的走线等等,对布局方面也是非常的关注。布局规划的重要性: 在PCB设计中,布局规划是至关重要的一步。它直接影响到后续布线的难易程度、信号完整性以及电磁兼容性等方面。因此,在进行元件布局之前,我们必须对PCB的平
    戈壁滩上绽放 2024-12-05 19:43 54浏览
  • 延续前一篇「抢搭智慧家庭生态圈热潮(一) 充满陷阱的产品介绍」系列文章,购买智能家电时需留意是否标有Works With Alexa (WWA)标章,然而,即使有了WWA标章后,产品难道就不会发生问题了吗?本篇由百佳泰将重点探讨在Alexa智能家居设备应用的实验中所遭遇到的问题。智能家庭隐忧浮现:智能家电APP使用状态不同步在先前的文章中,我们有提过建构Alexa智能家庭的三个主要元素:Alexa Built-in Devices(ABI)、Alexa Connected Device,以及Al
    百佳泰测试实验室 2024-12-05 15:26 37浏览
  • 本文介绍RK3566/RK3568开发板Android11系统,编译ROOT权限固件的方法。触觉智能Purple Pi OH鸿蒙开发板演示,搭载了瑞芯微RK3566四核处理器,Laval鸿蒙社区推荐开发板,已适配全新OpenHarmony5.0 Release系统,SDK源码全开放!关闭Selinux修改以下路径文件:adevice/rockchip/common/BoardConfig.mk修改代码如下:BOARD_BOOT_HEADER_VERSION ?= 2BOARD_MKBOOTIMG
    Industio_触觉智能 2024-12-05 10:27 16浏览
  • 现在最热门的AI PC,泛指配备了人工智能AI的个人电脑,虽然目前的AI功能大多仅运用于增加个人电脑的运算力及用户使用体验。然而,各家AI PC厂商/品牌商却不约而同针对Webcam的AI功能大作文章,毕竟这是目前可以直接让消费者感受到、最显著、也是最有感觉的应用情境!目前各家推出Webcam 的AI功能包括有:● 背景虚化● 面部识别和追踪。● 自动调节● 虚拟化和滤镜● 安全和隐私面临的困境:惊吓大于惊喜的AI优化调校由于每款AI PC的相机都有自己的设定偏好及市场定位,一旦经过AI的优化调
    百佳泰测试实验室 2024-12-05 15:30 39浏览
  • 车前大灯总成是一个集成了多种灯光功能的复杂系统,由于功能需求不同,其内部的灯珠串联或并联的数量也会有所差异。通过采用BOOST CV+BUCK CC两级供电方式,大灯控制器能够更好地适应智能大灯系统的需求,确保在各种负载瞬态变化下,大灯都能获得稳定、合适的电力供应。在汽车上,电池提供的电压通常是12V或24V,但是车大灯可能需要一个更稳定、更适合它工作的电压。这时候,DC/DC Converter就派上用场了。它可以把电池提供的电压转换成车大灯需要的电压,确保车大灯能够稳定、明亮地发光。此时就需
    时源芯微 2024-12-04 17:46 21浏览
  • CS5466AUUSB-C  (2lanes)to HDMI2.1 8K@30HZ(4K@144) +PD3.1  CS5563DP  (4lanes) to HDMI2.1 10k@60Hz CS5565USB-C  (4lanes) to HDMI2.1 10k@60Hz CS5569USB-C (4lanes) to HDMI2.1 10k@60Hz +PD3.1CS5228ANDP++ to HDMI(4K
    QQ1540182856 2024-12-05 15:56 77浏览
  • 在电子工程领域,高速PCB设计是一项极具挑战性和重要性的工作。随着集成电路的迅猛发展,电路系统的复杂度和运行速度不断提升,对PCB设计的要求也越来越高。在这样的背景下,我有幸阅读了田学军老师所著的《高速PCB设计经验规则应用实践》一书,深感受益匪浅。以下是我从本书中学习到的新知识和经验分享,重点涵盖特殊应用电路的PCB设计、高速PCB设计经验等方面。一、高速PCB设计的基础知识回顾与深化 在阅读本书之前,我对高速PCB设计的基础知识已有一定的了解,但通过阅读,我对这些知识的认识得到了进一步的深
    金玉其中 2024-12-05 10:01 163浏览
  • ~同等额定功率产品尺寸小一号,并保证长期稳定供应~全球知名半导体制造商ROHM(总部位于日本京都市)在其通用贴片电阻器“MCR系列”产品阵容中又新增了助力应用产品实现小型化和更高性能的“MCRx系列”。新产品包括大功率型“MCRS系列”和低阻值大功率型“MCRL系列”两个系列。在电子设备日益多功能化和电动化的当今世界,电子元器件的小型化和性能提升已成为重要课题。尤其是在汽车市场,随着电动汽车(xEV)的普及,电子元器件的使用量迅速增加。另外,在工业设备市场,随着设备的功能越来越多,效率越来越高,
    电子资讯报 2024-12-05 17:03 51浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦