[导读]作者:LiamHuang最近在讨论多线程编程中的一个可能的falsesharing问题时,有人提出加volatile可能可以解决问题。这种错误的认识荼毒多年,促使我写下这篇文章。约定Volatile这个话题,涉及到计算机科学多个领域多个层次的诸多细节。仅靠一篇博客,很难穷尽这些细...
作者: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
本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
在嵌入式系统开发中,整型溢出是引发安全漏洞和系统故障的常见原因。据MITRE统计,CWE-190(整数溢出)位列嵌入式安全漏洞前三。本文从工程实践角度,探讨边界检查算法与数据类型选择的协同防护策略。
关键字:
边界检查算法
嵌入式系统
整型溢出
在嵌入式系统广泛应用的今天,网络通信已成为其不可或缺的功能。然而,受限于资源、功耗和实时性要求,嵌入式系统中的TCP/IP协议栈性能优化成为关键挑战。本文将从协议栈选型、参数调优、硬件加速及代码优化等方面,探讨嵌入式系统...
关键字:
网络协议栈
嵌入式系统
在资源受限的嵌入式设备(如MCU、低功耗AI芯片)上部署深度学习模型时,需解决存储占用、计算延迟、功耗限制三大挑战。TinyML通过模型量化与推理加速技术,将ResNet、MobileNet等模型压缩至KB级,实现边缘设...
关键字:
TinyML
嵌入式AI
在嵌入式系统资源受限与功能扩展的双重压力下,模块化开发已成为提升软件可维护性的核心策略。通过将系统拆分为独立功能模块,结合清晰的接口定义与分层架构,可在STM32等MCU上实现代码复用率提升40%、缺陷修复周期缩短60%...
关键字:
模块化开发
软件架构设计
在嵌入式系统、工业物联网等各类电子设备中,UART与网口是两种应用广泛的通信接口,前者作为经典的串行通信接口,承担着简单设备互联、调试日志传输等基础任务,后者则专注于高速、远距离的数据交互,是设备接入网络、实现大数据量传...
关键字:
嵌入式
通信接口
网口通讯
在资源受限的嵌入式场景中,根文件系统(RootFS)的体积与功耗直接影响产品成本与用户体验。基于Yocto构建的轻量级根文件系统,通过精准裁剪与动态功耗管理,可将系统体积压缩至30MB以内,同时降低30%以上的待机功耗。...
关键字:
Yocto
根文件
RootFS
在嵌入式硬件调试中,时钟抖动和电源轨噪声是影响系统稳定性的两大关键因素。示波器作为核心调试工具,通过其高级触发、频谱分析和眼图测试功能,可精准定位问题根源。本文以泰克MDO4000C系列示波器为例,解析时钟抖动与电源噪声...
关键字:
示波器
嵌入式硬件
时钟抖动
嵌入式系统开发中,硬件与软件高度耦合,复杂度高,一次性集成所有模块调试极易陷入“问题定位难、复现率低”的困境。分步调试法通过“最小功能验证→模块逐步扩展→多模块协同”的渐进式策略,可显著提升调试效率。本文以STM32微控...
关键字:
嵌入式系统
分步调试法
在嵌入式系统向智能化、高性能化演进的浪潮中,RISC-V开源指令集架构凭借其模块化设计和可扩展性,成为硬件加速领域的重要推动力。结合FPGA的可重构特性,基于RISC-V的硬件乘法器实现方案正逐步打破传统架构的性能瓶颈,...
关键字:
RISC-V
FPGA
在物联网设备、可穿戴设备等嵌入式场景中,电池寿命是制约产品竞争力的核心指标。低功耗设计需贯穿硬件选型、系统架构到软件策略的全流程,其中休眠模式切换与电源管理芯片(PMIC)的精细配置是关键环节。本文从实际工程角度,解析如...
关键字:
低功耗设计
PMIC配置
嵌入式系统
在嵌入式系统开发中,传统软件断点依赖指令替换,易受优化代码或ROM存储限制,而JTAG调试器通过硬件断点与内存监控功能,可突破这些瓶颈,实现精准调试。本文结合ARM Cortex-M与RISC-V架构实践,解析JTAG在...
关键字:
JTAG调试器
硬件断点设置
嵌入式软件
在物联网设备固件升级过程中,未授权修改或恶意代码注入可能导致设备失控、数据泄露等严重后果。通过RSA-2048签名验证结合硬件安全模块(HSM)的防篡改设计,可在STM32H7系列MCU上实现99.997%的攻击拦截率。...
关键字:
固件升级
签名验证
物联网
在嵌入式系统开发中,性能优化与功耗控制是相互制约的核心挑战。通过对STM32F4系列MCU的实测分析,发现通过针对性代码优化可使计算密集型任务执行时间缩短62%,而结合精准功耗测量可进一步降低系统能耗35%。本文结合具体...
关键字:
嵌入式系统
代码优化
在嵌入式系统开发中,信号完整性直接影响系统稳定性。示波器作为硬件调试的核心工具,其200MHz带宽以上型号可捕捉纳秒级时序异常,成为破解SPI通信故障、电源纹波超标等难题的关键。本文结合Rigol DS1054Z与Tek...
关键字:
示波器
信号分析
在物联网设备数量突破500亿台的今天,嵌入式固件的安全性已成为保障数据隐私的核心挑战。AES(高级加密标准)凭借其抗量子计算攻击的128/192/256位密钥体系,成为嵌入式安全领域的首选算法。本文将解析AES在资源受限...
关键字:
AES算法
嵌入式固件
在资源受限的嵌入式系统中,存储管理直接影响系统稳定性与能效。内存池通过预分配机制消除动态内存碎片,而Flash存储策略则通过磨损均衡延长器件寿命。本文结合实战案例,解析两种技术的协同优化方法。
关键字:
嵌入式存储
Flash存储
在嵌入式系统开发中,图形界面(GUI)的构建常面临硬件资源受限、开发周期紧张等挑战。Adafruit GFX库凭借其跨平台兼容性、轻量化设计和丰富的API,成为开发者快速实现专业级图形界面的利器。本文将从实战角度解析该库...
关键字:
Adafruit GFX
图形界面
GUI
在嵌入式系统开发中,实时操作系统(RTOS)的选择直接影响项目开发效率、系统性能及维护成本。FreeRTOS与Zephyr作为两大主流RTOS,分别代表“轻量级精简设计”与“模块化物联网生态”两种技术路线。本文从架构特性...
关键字:
RTOS
FreeRTOS
Zephyr
在嵌入式Linux开发中,开发者常面临目标设备资源受限(如ARM Cortex-A系列处理器、低内存配置)的挑战,无法直接在设备上完成代码编译与调试。交叉编译与远程调试技术通过“宿主机-目标机”分离架构,将编译与调试任务...
关键字:
嵌入式Linux
交叉编译
远程调试
在物联网与工业4.0深度融合的背景下,嵌入式系统作为关键基础设施,其通信协议栈的性能直接影响系统实时性、可靠性和安全性。然而,受限于资源约束与硬件特性,传统协议栈在嵌入式场景中常面临内存拷贝、锁竞争、缓存效率低下等瓶颈。...
关键字:
通信协议栈
嵌入式系统