通过访问function访问共享内存需要`volatile`吗?

[ 编辑 ]对于背景阅读,并且要清楚,这就是我所说的: volatile关键字简介

在查看嵌入式系统代码时,我看到的最常见错误之一是省略了线程/中断共享数据的volatile。 但是我的问题是,当通过访问函数或成员函数访问变量时,是否“安全”不使用volatile

一个简单的例子; 在以下代码中……

 volatile bool flag = false ; void ThreadA() { ... while (!flag) { // Wait } ... } interrupt void InterruptB() { flag = true ; } 

…变量flag必须是易失性的,以确保ThreadA中的读取未被优化,但是如果通过函数读取标志…

 volatile bool flag = false ; bool ReadFlag() { return flag } void ThreadA() { ... while ( !ReadFlag() ) { // Wait } ... } 

…… flag仍然需要波动吗? 我意识到它不易受到伤害,但我担心的是省略它并且没有发现遗漏; 这会安全吗?

上面的例子是微不足道的; 在真实的情况下(以及我要求的原因),我有一个包装RTOS的类库,这样就有一个抽象类cTask来自任务对象。 这样的“活动”对象通常具有访问数据的成员函数,而不是在对象的任务上下文中修改但可以从其他上下文访问; 那么这些数据被声明为volatile是否至关重要?

我真的很感兴趣的是这些数据的保证 ,而不是实际编译器可能做的事情。 我可能会测试一些编译器并发现它们从未通过访问器优化读取,但有一天会发现编译器或编译器设置使得这个假设不真实。 我可以想象,例如,如果函数是内联的,那么这样的优化对于编译器来说是微不足道的,因为它与直接读取没有什么不同。

我对C99的解读是,除非你指定volatile ,否则实际访问变量的方式和时间是实现定义的。 如果指定volatile限定符,则代码必须根据抽象机器的规则工作。

标准中的相关部分是: 6.7.3 Type qualifiers (易失性描述)和5.1.2.3 Program execution (抽象机定义)。

一段时间以来,我知道许多编译器实际上都有启发式方法来检测应该重新读取变量以及何时可以使用缓存副本的情况。 易失性使编译器清楚地知道对变量的每次访问实际上都应该是对内存的访问。 没有volatile,似乎编译器可以自由地重新读取变量。

并且在函数中包装访问的BTW不会改变,因为即使没有inline的函数仍然可以由当前编译单元内的编译器内联。

对于C ++,可能值得检查前者基于的C89。 我手头没有C89。

是的,这很重要。
就像你说的volatile阻止代码破坏共享内存优化[C++98 7.1.5p8]
由于您永远不知道给定编译器现在或将来可能执行何种优化,因此应明确指定您的变量是volatile。

当然,在第二个例子中,省略了写/修改变量’flag’。 如果从未写入,则不需要它是易失性的。

关于主要问题

即使每个线程通过相同的函数访问/修改它,该变量仍然被标记为volatile。

一个函数可以在多个线程中同时“激活”。 想象一下,function代码只是一个线程获取并执行的蓝图。 如果线程B中断线程A中ReadFlag的执行,它只执行ReadFlag的不同副本(具有不同的上下文,例如不同的堆栈,不同的寄存器内容)。 通过这样做,它可能会搞乱线程A中ReadFlag的执行。

在C中,此处不需要volatile关键字(在一般意义上)。

从ANSI C规范(C89),A8.2节“类型说明符”:

volatile对象没有与实现无关的语义。

Kernighan和Ritchie对此部分的评论(指constvolatile说明符):

除了它应该诊断显式尝试更改const对象之外,编译器可能会忽略这些限定符。

鉴于这些细节,您无法保证特定编译器如何解释volatile关键字,或者它是否完全忽略它。 完全依赖于实现的关键字在任何情况下都不应被视为“必需”。

话虽如此,K&R还说:

volatile的目的是强制实现抑制否则可能发生的优化。

实际上,这就是我所看到的每个编译器实际上解释volatile 。 将变量声明为volatile ,编译器不会尝试以任何方式优化对它的访问。

大多数情况下,现代编译器非常适合判断变量是否可以安全地缓存。 如果您发现您的特定编译器正在优化它不应该的东西,那么添加volatile关键字可能是合适的。 但请注意,这可能会限制编译器可以对使用volatile变量的函数中的其余代码执行的优化量。 有些编译器比其他编译器更好; 我使用的一个嵌入式C编译器将关闭访问volatile的函数的所有优化,但像gcc这样的其他人似乎仍能执行一些有限的优化。

通过访问器函数访问变量应该阻止函数缓存该值。 即使该函数是自动内联的,每次调用该函数都应该重新调用该函数并重新获取一个新值。 我从未见过编译器会自动内联访问器函数,然后优化数据重新获取。 我不是说它不会发生(因为这是依赖于实现的行为),但我不会编写任何期望发生的代码。 您的第二个示例实际上是围绕变量放置一个包装器API,而库在不使用volatile情况下执行此操作。

总而言之,C中volatile对象的处理依赖于实现。 根据ANSI C89规范,没有任何“保证”。

您的代码在线程和中断例程之间共享volatile对象。 没有编译器实现(我曾经见过)给予足够的volatile能力来处理并行访问。 您应该使用某种锁定机制来保证两个线程(在您的第一个示例中)不会互相踩到脚趾(即使一个是中断处理程序,您仍然可以在多CPU或多个线程上进行并行访问 – 核心系统)。

编辑:我没有仔细阅读代码,因此我认为这是一个关于线程同步的问题,因为永远不应该使用volatile ,但这种用法看起来可能没问题(取决于所讨论的变量的其他方式)使用,如果中断总是在运行,那么它的内存视图(cache-)与线程所看到的一致。如果你将它包装在一个函数调用中,你可以删除volatile限定符吗?接受的答案是正确的,你不能。我会留下原来的答案,因为读这个问题的人知道在某些非常特殊的情况下, volatile几乎是无用的。

更多编辑:您的RTOS用例可能需要额外的保护,除了易失性之外,您可能需要在某些情况下使用内存屏障或使它们成为primefaces…我无法确切地告诉您,这只是您需要的东西小心(我建议看看我下面的Linux内核文档链接,Linux不会使用volatile这类事情,很可能是有充分理由的)。 当你执行并且不需要volatile非常强烈地依赖于你正在运行的CPU的内存模型,并且通常volatile是不够的。

volatile是执行此操作的错误方法,它不保证此代码可以正常工作,它不适用于此类用途。

volatile用于读/写内存映射设备寄存器,因此它就足够用于此目的,但是当你谈论线程之间的东西时它没有帮助。 (特别是编译器仍然大声重新排序一些读取和写入,就像CPU在执行时一样(这个非常重要,因为volatile不会告诉CPU做任何特殊的事情(有时它意味着绕过缓存,但那是编译器/ CPU依赖))

请参阅http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2016.html , 英特尔开发人员文章 , CERT , Linux内核文档

这些文章的简短版本, volatile使用你想要的方式是BAD WRONG。 糟糕,因为它会使你的代码变慢,错误,因为它实际上并没有你想要的。

实际上,在x86上,你的代码可以在有或没有volatile情况下正常运行,但是它将是不可移植的。

编辑:注意自己实际读取代码…这 volatile的意思。