在C语言中,volatile关键字应该在哪些地方使用?

3

我知道volatile关键字可以防止编译器优化变量并在每次读取时从内存中读取。除了内存映射寄存器,还有哪些情况需要使用volatile?如果使用符合标准的编译器,在这两种情况下是否必须将test_var声明为volatile?

1.

在file1.c文件中

int test_var=100;


void func1()
{
    test_var++;
}

在file2.c文件中

extern int test_var;

void func2()
{
    if(test_var==100)
    {
      ....
    }
}

2.

在file1.c文件中

int test_var=100;

void func1()
{

}

在file2.c文件中

extern int test_var;

void func2()
{
    if(test_var==100)
    {
      ....
    }
}

这在多线程应用程序中可能也是必需的。 - Jean-François Fabre
3
请注意,如果您声明变量为extern存储类,则编译器在优化读写该变量的能力方面受到限制。 - tofro
@Jean-FrançoisFabre,那是一个经典的bug,请看我的回答。 - user2371524
@FelixPalmen 非常好的回答,顺便说一下。 - Jean-François Fabre
1
https://barrgroup.com/Embedded-Systems/How-To/C-Volatile-Keyword - Ouss4
编译器在优化该变量的读写时受到限制,具体是怎样的呢? - curiousguy
5个回答

6

内存映射I/O是C中唯一通用使用volatile的方法。*)

在POSIX信号中,volatile也可以与类型sig_atomic_t一起使用,如下:

volatile sig_atomic_t signal_occured = 0;

你的两种情况都不需要使用volatile。如果你只是想要保证值在不同编译单元之间更新,可以看看tofro的评论,extern已经保证了这一点。特别地,在C语言中,volatile不是线程同步的正确工具。它只会引入错误,因为正如你所说,它确实需要对变量进行实际的读写访问,但它并不能强制执行它们与线程的正确顺序(缺少内存屏障,请搜索详细信息)。
请注意,这与其他一些语言中的volatile设计用于在线程之间工作是不同的。
在嵌入式系统中,当与原子读/写的数据类型结合使用时,volatile可能足够用于ISR(中断服务例程)和主程序之间的通信,就像POSIX信号的sig_atomic_t一样。请查阅您的编译器文档。
*) C标准在脚注中提到了这一点,以及“异步中断函数”的用例,因为内存映射I/O超出了语言的范围。语言只是以使其适用于内存映射I/O的方式定义了volatile的语义。

1
鉴于您似乎在区分标准C和POSIX(或其他)C扩展,您的开头句有点奇怪。据我所知,标准C不提供内存映射I/O,因此您不应该说“在C中没有通用使用volatile”吗? - John Bollinger
1
@JohnBollinger,好的,在第6.7.3节的脚注134中解释了volatile的预期用途(“可以使用volatile声明来描述对应于内存映射输入 / 输出端口的对象”),但它还提到了“异步中断函数”(使用POSIX信号的用例),所以你可能是正确的 ;) - user2371524
由于@JohnBollinger最有可能是将volatile用于内存映射I/O,因此我会保留它并添加一个脚注。 - user2371524
这个回答是误导性的。在C标准中,明确使用了volatile,即用于信号处理程序和longjmp,请参见我的答案。 - Jens Gustedt

4
在您的两个示例中,都不需要使用volatilevolatile是必需的:
1.任何变量可能在单个执行线程的控制之外被更改的地方, 2.任何需要发生变量访问的地方,即使语义上没有影响也需要访问。
情况1包括:
- 内存映射I/O寄存器, - 用于DMA传输的内存, - 在中断和/或线程上下文之间共享的内存, - 独立处理器之间共享的内存(例如双端口RAM)
情况2包括:
- 用于空延迟循环的循环计数器,否则整个循环可能被完全优化掉而不花费任何时间, - 仅写入但从未读取以在调试器中观察的变量。
以上示例可能不是详尽无遗的,但关键是volatile的语义; 语言只需根据源代码指示执行显式访问。

例子“在中断和/或线程上下文之间共享内存”以及“在独立处理器(例如双口RAM)之间共享内存”在C语言中是危险的。简而言之:这些情况需要某些东西,但通常不仅仅是volatile。一般来说,volatile对于这些情况是无用的。 - user2371524
1
@FelixPalmen:我不确定“无用”完全正确——肯定不是完整的解决方案。仅仅假设使用volatile就可以解决问题是危险的;然而,volatile仍然在某些情况下发挥作用。 - Clifford
@FelixPalmen 危险并不总是无用的。 - Bumsik Kim
1
可以在http://www.embedded.com/design/programming-languages-and-tools/4442490/1/C-keywords--Don-t-flame-out-over-volatile找到比在SO上更详尽的文章,以及关于嵌入式系统中volatile的基础知识,请参见http://www.embedded.com/electronics-blogs/beginner-s-corner/4023801/Introduction-to-the-Volatile-Keyword。 - Clifford
@FelixPalmen:在SO评论中解释“多余”评论可能不太方便,我需要澄清一下。如果您认为有什么重要的东西需要添加(或者可能删除),请随意编辑我的答案-我会感激的。当然,“多余”并不危险。同步和原子性问题是volatile未涉及的重要方面,但这些可能不是问题的主题。 - Clifford
显示剩余6条评论

1
在使用中断的C微控制器应用程序中,volatile关键字是确保在中断中设置的值能够正确保存并在主处理循环中具有正确值的必要条件。如果没有使用volatile,那么定时器中断或ADC(模拟数字转换)中断等可能会在处理器状态在中断后返回时出现损坏的情况,这可能是最大的原因。Atmel和GCC提供了一个经典的模板:
volatile uint8_t flag = 0;

ISR(TIMER_whatever_interrupt)
{
    flag = 1;
}

while(1) // main loop
{
    if (flag == 1)
    {
        <do something>
        flag = 0;
    }
}

如果没有使用volatile,就不能保证其按预期工作。


1
除了像内存映射设备这样的扩展之外,在标准C中,volatile有两个用例:与信号处理程序的交互和在使用setjmp/longjmp期间修改对象。这两种情况都是控制流程不寻常的情况,优化器可能不会意识到。

内存映射设备并不是"扩展",它们只是可能存在于"外部"的环境属性,并且它们 已经 在标准中提到。 - user2371524

0
除了内存映射寄存器之外,我们在哪些情况下需要使用volatile?
如果:
1.执行是纯顺序的(没有线程和异步传递的信号); 2.您不使用longjmp; 3.您不需要能够调试使用优化编译的程序; 4.您不使用具有模糊指定语义的结构,例如浮点运算; 5.您不进行无用的计算(计算结果被忽略),例如基准循环中的计算; 6.您不对任何纯计算进行计时,即任何非基于I/O的计算(基于I/O的计时,例如访问网络请求、外部数据库访问的时间)。
那么您可能不需要使用volatile。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接