Volatile变量
编程程序中的特殊代码
在程序设计中,尤其是在C语言C++C#Java语言中,使用volatile关键字声明的变量对象通常具有与优化、多线程相关的特殊属性。通常,volatile关键字用来阻止(伪)编译器认为的无法“被代码本身”改变的代码(变量/对象)进行优化。如在C语言中,volatile关键字可以用来提醒编译器它后面所定义的变量随时有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
C和C++中的volatile
在C,以及C++中,volatile关键字的作用:
根据相关的标准(C,C++,POSIX,WIN32)和绝大多数实现,对volatile变量的操作并不是原子的,也不能用来为线程建立严格的happens-before关系。volatile关键字就像便携式线程构建一样基本没什么用处。
Visual C++2005 保证volatile变量是一种内存屏障,阻止编译器和CPU重新安排读入和写出语义。在先前版本的Visual C++则没有此类保证。在其他方面将指针定义为volatile可能会影响程序的性能。例如,如果指针定义对代码的其他地方可见,强制编译器将指针视为屏障,就会降低程序的性能,这是完全不必要的。
对用户定义的非基本数据类型使用volatile
基本类型的对象用volatile修饰后,仍旧支持所有的操作(加、乘、赋值等)。但是,用户定义的非基本类型(class、struct、union)的对象被volatile修饰后,具有不同行为:
volatile与多线程语义
临界区内部,通过互斥锁(mutex)保证只有一个线程可以访问,因此临界区内的变量不需要是volatile的;而在临界区外部,被多个线程访问的变量应为volatile,这也符合了volatile的原意:防止编译器缓存(cache)了被多个线程并发用到的变量。volatile对象只能调用volatile成员函数,这意味着应仅对多线程并发安全的成员函数加volatile修饰,这种volatile成员函数可自由用于多线程并发或者重入而不必使用临界区;非volatile的成员函数意味着单线程环境,只应在临界区内调用。在多线程编程中可以令该数据对象的所有成员函数均为普通的非volatile修饰,从而保证了仅在进入临界区(即获得了互斥锁)后把该对象显式转为普通对象之后才能调用该数据对象的成员函数。这种用法避免了编程者的失误——在临界区以外访问共享对象的内容:
对于内建类型,不应直接用volatile,而应把它包装为结构的成员,就可以保护了volatile的结构对象不被不受控制地访问。
C语言中MMIO的例子
在这里例子中,代码将foo的值设置为0。然后开始不断地轮询它的值直到它变成255:
一个执行优化的编译器会提示没有代码能修改foo的值,并假设它永远都只会是0.因此编译器将用类似下列的无限循环替换函数体:
但是,foo可能指向一个随时都能被计算机系统其他部分修改的地址,例如一个连接到中央处理器的设备的硬件寄存器,上面的代码永远检测不到这样的修改。如果不使用volatile关键字,编译器将假设当前程序是系统中能改变这个值部分(这是到最广泛的一种情况)。 为了阻止编译器像上面那样优化代码,需要使用volatile关键字:
这样修改以后循环条件就不会被优化掉,当值改变的时候系统将会检测到。
C语言中的优化对比
下面的C程序和后面的汇编代码展示了volatile关键字如何影响编译器的输出。这里使用的编译器是GCC。
Java中的volatile
Java也支持volatile关键字,但它被用于其他不同的用途。当volatile用于一个作用域时,Java保证如下:
使用volatile会比使用更快,但是在一些情况下它不能工作。volatile使用范围在Java5中得到了扩展,特别是双重检查锁定能够正确工作。
参考资料
最新修订时间:2022-09-13 22:46
目录
概述
C和C++中的volatile
参考资料