[编辑]对于背景阅读,并且要清楚,这就是我所说的:Introduction to the volatile keyword
在查看嵌入式系统代码时,我看到的最常见错误之一是省略了线程/中断共享数据的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是否至关重要?
我真的对保证关于此类数据的内容感兴趣,而不是实际编译器可能做的事情。我可能会测试一些编译器并发现它们从未通过访问器优化读取,但有一天会发现编译器或编译器设置使得这个假设不真实。我可以想象,例如,如果函数是内联的,那么对于编译器来说这样的优化是微不足道的,因为它与直接读取没有什么不同。
答案 0 :(得分:12)
我对C99的解读是,除非您指定volatile
,否则实际访问变量的方式和时间是实现定义的。如果指定volatile
限定符,则代码必须根据抽象机的规则工作。
标准中的相关部分是:6.7.3 Type qualifiers
(易失性描述)和5.1.2.3 Program execution
(抽象机器定义)。
一段时间以来,我知道许多编译器实际上都有启发式方法来检测应该重新读取变量以及何时可以使用缓存副本的情况。易失性使编译器清楚地知道对变量的每次访问实际上都应该是对内存的访问。没有volatile,似乎编译器可以自由地重新读取变量。
BTW包装函数中的访问并没有改变,因为即使没有inline
的函数仍然可能被当前编译单元内的编译器内联。
P.S。对于C ++,可能值得检查前者所基于的C89。我手头没有C89。
答案 1 :(得分:5)
是的,这很关键
就像你说volatile
阻止了共享内存[C++98 7.1.5p8]
上的代码破解优化
既然你永远不知道给定的编译器现在或将来可以做什么样的优化,你应该明确指出你的变量是易变的。
答案 2 :(得分:1)
当然,在第二个例子中,省略了写/修改变量'flag'。如果从未写入,则不需要它是易变的。
关于主要问题
即使每个线程通过相同的函数访问/修改变量,变量仍然会被标记为volatile。
一个函数可以在多个线程中同时“激活”。想象一下,功能代码只是一个线程获取并执行的蓝图。如果线程B中断线程A中ReadFlag的执行,它只是执行ReadFlag的不同副本(具有不同的上下文,例如不同的堆栈,不同的寄存器内容)。通过这样做,它可能会搞乱线程A中ReadFlag的执行。
答案 3 :(得分:1)
在C中,此处不需要volatile
关键字(在一般意义上)。
从ANSI C规范(C89),A8.2节“类型说明符”:
没有 与实现无关的语义 对于
volatile
个对象。
Kernighan and Ritchie对此部分发表评论(指const
和volatile
说明符):
除了它应该诊断 明确尝试改变
const
对象,编译器可能会忽略这些 限定符。
鉴于这些细节,您无法保证特定编译器如何解释volatile
关键字,或者它是否完全忽略它。在任何情况下都不应将完全依赖于实现的关键字视为“必需”。
话虽如此,K& R也表示:
volatile
的目的是强迫 压制的实现 否则可以进行优化 发生。
实际上,这就是我见过的所有编译器解释volatile
的方式。将变量声明为volatile
,编译器不会尝试以任何方式优化对它的访问。
大多数情况下,现代编译器非常适合判断变量是否可以安全地缓存。如果您发现您的特定编译器正在优化它不应该的东西,那么添加volatile
关键字可能是合适的。但请注意,这可能会限制编译器可以对使用volatile
变量的函数中的其余代码执行的优化量。有些编译器比其他编译器更好;我使用的一个嵌入式C编译器将关闭访问volatile
的函数的所有优化,但像gcc这样的其他编译器似乎仍能执行一些有限的优化。
通过访问器函数访问变量应该阻止函数缓存该值。即使该函数是自动内联的,每次调用该函数都应该重新调用该函数并重新获取一个新值。我从未见过编译器会自动内联访问器函数,然后优化数据重新获取。我不是说它不会发生(因为这是依赖于实现的行为),但我不会编写任何期望发生的代码。您的第二个示例实际上是围绕变量放置一个包装器API,而库在不使用volatile
的情况下执行此操作。
总而言之,C中volatile
个对象的处理依赖于实现。根据ANSI C89规范,没有任何“保证”。
您的代码在线程和中断例程之间共享volatile
对象。没有编译器实现(我曾经见过)给volatile
足够的功率来处理并行访问。您应该使用某种locking mechanism来保证两个线程(在您的第一个示例中)不会互相踩到脚趾(即使一个是中断处理程序,您仍然可以在多个线程上进行并行访问CPU或多核系统)。
答案 4 :(得分:0)
编辑:我没有仔细阅读代码,所以我认为这是一个关于线程同步的问题,为什么永远不应该使用volatile
,但这种用法看起来可能没问题(取决于如何使用有问题的变量,并且如果中断总是在运行,使得它的内存视图(缓存 - )与线程所看到的一致。在“你可以删除volatile
的情况下限定词,如果你把它包装在一个函数调用中?'接受的答案是正确的,你不能。我会留下原来的答案,因为读这个问题的人知道volatile
几乎没用某些特殊情况。
更多编辑:您的RTOS用例可能需要额外的保护,除了volatile之外,您可能需要在某些情况下使用内存屏障或使它们成为原子...我无法确切地告诉您,这只是您的事情需要注意的是(我建议看一下我下面的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,Intel developer article,CERT,Linux kernel documentation
这些文章的简短版本,volatile
使用您想要的方式是BAD 和错误。糟糕,因为它会使你的代码变慢,错误,因为它实际上并没有你想要的。
实际上,在x86上,无论有没有volatile
,您的代码都能正常运行,但将不可移植。
编辑:注意自己实际阅读代码......这个 是volatile
的意思。