当前位置:首页 > 嵌入式 > 技术让梦想更伟大
[导读]while (true)



作者: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
欲知详情,请下载word文档 下载文档
换一批

延伸阅读

[技术让梦想更伟大] 不要再误解C volatile了

不要再误解C   volatile了

作者:Liam Huang 最近在讨论多线程编程中的一个可能的 false sharing 问题时,有人提出加 volatile 可能可以解决问题。这种错误的认识荼毒多年,促使我写下这篇文章。...

关键字: volatile

[技术让梦想更伟大] 不要再误解C  volatile了

不要再误解C   volatile了

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

关键字: volatile

[嵌入式电路图] C语言类型修饰符Volatile的使用

volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。 vola...

关键字: C语言 单片机制作 修饰符 volatile

[嵌入式教程] 嵌入式软件工程师必须知道的:volatile的作用

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的...

关键字: 硬件 寄存器 技术教程 编译器优化 volatile

[单片机] C语言中volatile关键字的使用

  volatile的意思是易变的、可变的,作用是限制编译器优化某些变量。首先看一段C51程序:  Keil在优化级别是为8时得到如下汇编代码(部分未列出):  可以看到,变量d的值赋给x,y,z时,只有x中是直接...

关键字: 语言 关键字 volatile

技术子站

关闭