为什么在C中需要volatile?
为什么在C中需要volatile
? 它是干什么用的? 它会做什么?
Volatile告诉编译器不要优化任何与volatilevariables有关的事情。
只有一个理由使用它:当你与硬件接口。
比方说,你有一个硬件映射到某个地方的RAM,有两个地址:一个命令端口和一个数据端口:
typedef struct { int command; int data; int isbusy; } MyHardwareGadget;
现在你想发送一些命令:
void SendCommand (MyHardwareGadget * gadget, int command, int data) { // wait while the gadget is busy: while (gadget->isbusy) { // do nothing here. } // set data first: gadget->data = data; // writing the command starts the action: gadget->command = command; }
看起来很容易,但可能会失败,因为编译器可以自由地更改写入数据和命令的顺序。 这会导致我们的小工具使用之前的数据值发布命令。 也看看等待忙碌的循环。 那一个会被优化出来。 编译器会尝试聪明,只读一次isbusy的值,然后进入无限循环。 这不是你想要的。
解决这个问题的方法是将指针小工具声明为volatile。 这样编译器就被迫去做你写的东西。 它不能删除内存分配,也不能caching寄存器中的variables,也不能改变分配的顺序:
这是正确的版本:
void SendCommand (volatile MyHardwareGadget * gadget, int command, int data) { // wait while the gadget is busy: while (gadget->isbusy) { // do nothing here. } // set data first: gadget->data = data; // writing the command starts the action: gadget->command = command; }
另一个用于volatile
是信号处理程序。 如果你有这样的代码:
quit = 0; while (!quit) { /* very small loop which is completely visible to the compiler */ }
编译器可以注意到,循环体不会触及quit
variables,并将循环转换为while (true)
循环。 即使在SIGINT
和SIGTERM
的信号处理程序上设置了quit
variables, 编译器无法知道这一点。
但是,如果quit
variables被声明为volatile
,则编译器每次都被迫加载它,因为它可以在其他地方修改。 这正是你想要的这种情况。
C中的volatile
实际上是为了不自动cachingvariables的值而存在的。 它会告诉机器不要caching这个variables的值。 因此,每次遇到它时,都会从主内存中获取给定volatile
variables的值。 这个机制被使用,因为在任何时候该值都可以被操作系统或任何中断修改。 所以使用volatile
会帮助我们每次重新访问这个值。
volatile
告诉编译器,你的variables可以通过其他方式进行更改,而不是访问它的代码。 例如,它可能是一个I / O映射的内存位置。 如果在这种情况下没有指定它,则可以优化一些variables访问,例如,其内容可以保存在寄存器中,并且内存位置不会被再次读回。
请参阅Andrei Alexandrescu撰写的“ volatile – multithreading程序员最好的朋友 ”
volatile关键字的devise目的是为了防止在某些asynchronous事件出现时编译器优化可能导致代码不正确。 例如,如果将一个原始variables声明为volatile ,则不允许编译器将其caching在一个寄存器中 – 这是一个常见的优化,如果该variables是在多个线程之间共享的话,这将是灾难性的。 所以一般的规则是,如果你有多个线程必须共享的原始types的variables,那么声明这些variables是不稳定的 。 但是你可以用这个关键字做更多的事情:你可以使用它来捕获不是线程安全的代码,并且你可以在编译时这样做。 这篇文章展示了它是如何完成的。 该解决scheme涉及一个简单的智能指针,也可以很容易地序列化关键代码段。
本文适用于C
和C++
。
另见Scott Meyers和Andrei Alexandrescu撰写的文章“ C ++和双重locking的危险 ”
因此,在处理某些内存位置时(例如内存映射端口或由ISR [中断服务例程]引用的内存),必须暂停某些优化。 (1)volatilevariables的内容是“unstable”(可以通过编译器未知的方式改变),(2)所有对volatile数据的写操作都是“observable”,所以它们是可观察的(3)所有对易失性数据的操作都按照它们出现在源代码中的顺序执行。 前两条规则确保正确的阅读和写作。 最后一个允许执行混合input和输出的I / O协议。 这是非正式的C和C ++的不稳定保证。
我简单的解释是:
在某些情况下,编译器会根据逻辑或代码对其认为不会改变的variables进行优化。 volatile
关键字可防止优化variables。
例如:
bool usb_interface_flag = 0; while(usb_interface_flag == 0) { // execute logic for the scenario where the USB isn't connected }
从上面的代码中,编译器可能会认为usb_interface_flag
被定义为0,在while循环中它永远是零。 优化之后,编译器会一直处理while(true)
,导致无限循环。
为了避免这种情况,我们把标志声明为volatile,我们告诉编译器这个值可能被外部接口或其他程序模块改变,也就是说,请不要优化它。 这是挥发性的用例。
挥发性的边际用途如下。 假设你想计算函数f
的数值导数:
double der_f(double x) { static const double h = 1e-3; return (f(x + h) - f(x)) / h; }
问题是由于舍入误差, x+hx
通常不等于h
。 想一想:当你减去非常接近的数字时,就会损失大量的有效数字,从而毁掉导数的计算(想想1.00001-1)。 一个可能的解决方法可能是
double der_f2(double x) { static const double h = 1e-3; double hh = x + h - x; return (f(x + hh) - f(x)) / hh; }
但取决于您的平台和编译器开关,该function的第二行可能会被积极优化的编译器清除。 所以你写了
volatile double hh = x + h; hh -= x;
强制编译器读取包含hh的内存位置,从而放弃最终的优化机会。
我会提到挥发性物质很重要的另一种情况。
假设你为了更快的I / O而对文件进行内存映射,并且这个文件可以在后台更改(例如文件不在本地硬盘上,而是由另一台计算机通过networking传输)。
如果通过指向非易失性对象的指针(在源代码级别)访问内存映射文件的数据,那么编译器生成的代码可以多次获取相同的数据,而不必知道它。
如果这些数据发生改变,您的程序可能会使用两个或更多不同版本的数据并进入不一致的状态。 这不仅会导致程序在逻辑上不正确的行为,而且如果从不受信任的位置处理不受信任的文件或文件,还会导致程序中可利用的安全漏洞。
如果你关心安全,你应该考虑这个重要的情况。
有两个用途。 这些在embedded式开发中经常使用。
-
编译器不会优化使用volatile关键字定义的variables的函数
-
易失性用于访问RAM,ROM等中的精确内存位置。这更常用于控制内存映射设备,访问CPU寄存器和定位特定的内存位置。
查看汇编列表示例。 Re:在embedded式开发中使用C“volatile”关键字
当您要强制编译器不要优化特定的代码序列(例如,写一个微基准)时,易失性也是有用的。
易失性意味着存储可能在任何时候都发生变化,并且被改变,但是在用户程序的控制之外。 这意味着如果你引用这个variables,程序应该总是检查物理地址(即一个映射的inputFIFO),而不是以caching的方式使用它。
一个volatile可以从编译的代码之外改变(例如,一个程序可以将一个volatilevariables映射到一个内存映射的寄存器)。编译器不会将某些优化应用于处理volatilevariables的代码 – 例如,将其加载到寄存器中而不写入存储器。 处理硬件寄存器时这很重要。
Wiki说volatile
一切:
- 易失性(电脑编程)
而Linux内核的文档也对volatile
进行了很好的说明:
- 为什么不应该使用“volatile”types的类
它不允许编译器自动更改variables的值。 一个易变的variables是dynamic使用的。
在我看来,你不应该期望volatile
太大。 为了说明,请看尼尔斯·皮彭布林克(Nils Pipenbrinck)备受赞誉的答案 。
我会说,他的例子不适合volatile
。 volatile
仅用于: 防止编译器进行有用的和理想的优化 。 关于线程安全,primefaces访问甚至是内存顺序都没有关系。
在这个例子中:
void SendCommand (volatile MyHardwareGadget * gadget, int command, int data) { // wait while the gadget is busy: while (gadget->isbusy) { // do nothing here. } // set data first: gadget->data = data; // writing the command starts the action: gadget->command = command; }
在gadget->command = command
之前的gadget->data = data
仅在编译器的编译代码中得到保证。 在运行时,处理器仍然可能重新sorting数据和命令,关于处理器架构。 硬件可能会得到错误的数据(假设小工具被映射到硬件I / O)。 数据和命令分配之间需要内存屏障。