No, volatile
是无害的。在任何情况下。曾经。没有可能的格式良好的代码会因添加以下内容而中断volatile
到一个对象(以及指向该对象的指针)。然而,volatile
常常不被理解。内核文档指出的原因volatile
被认为有害的是人们继续以破坏的方式使用它来实现内核线程之间的同步。特别是,他们使用volatile
整数变量就好像对它们的访问保证是原子的,但事实并非如此。
volatile
也并非毫无用处,特别是如果你使用裸机,你will需要它。但是,与任何其他工具一样,理解其语义也很重要volatile
在使用它之前。
What volatile
is
进入volatile
在标准中,对象被认为是副作用与增加或减少相同的方式++
and --
。特别是,这意味着 5.1.2.3 (3),其中表示
(...)如果实际实现可以推断出其值未被使用并且不会产生所需的副作用(包括由调用函数或访问易失性对象引起的任何副作用),则实际实现不需要评估表达式的一部分
不适用。编译器必须丢弃它认为知道的有关 a 值的所有内容volatile
每个序列点都有变量。 (与其他副作用一样,当访问volatile
对象发生由序列点控制)
这样做的影响很大程度上是禁止某些优化。以代码为例
int i;
void foo(void) {
i = 0;
while(i == 0) {
// do stuff that does not touch i
}
}
编译器被允许使其成为一个从不检查的无限循环i
再次因为它可以推断出i
在循环中没有改变,因此i == 0
永远不会是假的。即使有另一个线程或中断处理程序可能会发生变化,这也成立i
。编译器不知道它们,也不关心它们。明确允许不关心。
将此与
int volatile i;
void foo(void) {
i = 0;
while(i == 0) { // Note: This is still broken, only a little less so.
// do stuff that does not touch i
}
}
现在编译器必须假设i
可以随时改变,不能做这个优化。当然,这意味着如果您处理中断处理程序和线程,volatile
对象是同步所必需的。然而,它们还不够。
What volatile
isn't
What volatile
不保证是原子访问。如果您习惯于嵌入式编程,这应该具有直观意义。如果愿意的话,请考虑以下 8 位 AVR MCU 的代码:
uint32_t volatile i;
ISR(TIMER0_OVF_vect) {
++i;
}
void some_function_in_the_main_loop(void) {
for(;;) {
do_something_with(i); // This is thoroughly broken.
}
}
该代码被破坏的原因是访问i
不是原子的——在 8 位 MCU 上不能是原子的。例如,在这个简单的情况下,可能会发生以下情况:
-
i
is 0x0000ffff
-
do_something_with(i)
即将被调用
- 高两个字节
i
被复制到此调用的参数槽中
- 此时定时器0溢出,主循环中断
- ISR 变化
i
。的低两个字节i
溢出,现在是0
. i
is now 0x00010000
.
- 主循环继续,低两个字节
i
被复制到参数槽中
-
do_something_with
被称为0
作为其参数。
类似的事情也可能发生在个人电脑和其他平台上。如果说有什么不同的话,那就是更复杂的架构可能会带来更多机会。
Takeaway
所以不,使用volatile
还不错,你(经常)必须用裸机代码来完成它。然而,当你使用它时,你必须记住,它不是一根魔杖,你仍然需要确保自己不会被绊倒。在嵌入式代码中,通常有一种特定于平台的方法来处理原子性问题;例如,对于 AVR,通常的 crowbar 方法是在持续时间内禁用中断,如下所示
uint32_t x;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
x = i;
}
do_something_with(x);
...其中ATOMIC_BLOCK
宏调用cli()
(禁用中断)之前和sei()
(启用中断)如果事先启用的话。
C11 是第一个明确承认多线程存在的 C 标准,引入了一系列新的原子类型和内存防护操作,可用于线程间同步,并且在许多情况下利用volatile
不必要。如果您可以使用它们,就使用它们,但它们可能需要一段时间才能到达所有常见的嵌入式工具链。有了它们,上面的循环就可以像这样修复:
atomic_int i;
void foo(void) {
atomic_store(&i, 0);
while(atomic_load(&i) == 0) {
// do stuff that does not touch i
}
}
...以其最基本的形式。更宽松的内存顺序语义的精确语义远远超出了 SO 答案的范围,因此我将在这里坚持使用默认的顺序一致的内容。
如果您对此感兴趣,Gil Hamilton 在评论中提供了一个链接,该链接指向使用 C11 原子的无锁堆栈实现的解释,尽管我不认为这是内存顺序语义本身的非常好的撰写。然而,C11 模型似乎与 C++11 内存模型密切相关,其中存在有用的演示here。如果我找到 C11 特定文章的链接,我稍后会将其放在这里。