volatile目录
前言
由于内存访问速度远不及CPU处理速度,为提高机器整体性能,在硬件上引入硬件高速缓存Cache,加速对内存的访问。另外在现代CPU中指令的执行并不一定严格按照顺序执行,没有相关性的指令可以乱序执行,以充分利用CPU的指令流水线,提高执行速度。以上是硬件级别的优化。再看软件一级的优化:一种是在编写代码时由程序员优化,另一种是由编译器进行优化。编译器优化常用的方法有:将内存变量缓存到寄存器;调整指令顺序充分利用CPU指令流水线,常见的是重新排序读写指令。对常规内存进行优化的时候,这些优化是透明的,而且效率很好。由编译器优化或者硬件重新排序引起的问题的解决办法是在从硬件(或者其他处理器)的角度看必须以特定顺序执行的操作之间设置内存屏障(memory barrier)。
一、volatile关键字
C语言中的 volatile 关键字被设计用来修饰变量,用于表明该变量是易变的。具体来说,它告诉编译器不要对该变量进行优化,因为该变量可能会在程序的执行过程中被外部因素改变。
volatile int counter = 0;
变量 counter 被声明为 volatile int 类型,表示该变量是易变的。这意味着,编译器不应该对该变量进行任何优化,而是应该每次都从内存中读取该变量的值。
1.为什么需要volatile关键字?
在编写 C语言程序时,编译器通常会对变量进行优化,以尽可能地提高程序的性能。例如,编译器可能会把一个变量的值缓存到寄存器中,以避免频繁地读写内存。这样做虽然可以提高程序的执行效率,但是它有一个重要的前提,就是该变量的值在程序的执行过程中不会被外部因素改变。
然而,在一些特定的场景中,变量的值可能会在程序的执行过程中被外部因素改变,比如:
- 在多线程程序中,多个线程可能会同时访问同一个变量;
- 在嵌入式系统中,变量的值可能会被硬件中断改变; 在使用内存映射
- I/O(Memory-Mapped I/O)的系统中,变量的值可能会被设备寄存器改变。
在这些情况下,如果编译器对变量进行优化,可能会导致程序出现不可预测的错误。因此,C语言引入了 volatile 关键字来告诉编译器,该变量是易变的,不应该进行优化
2.volatile关键字的作用
2.1防止编译器优化
编译器会对变量进行优化,以提高程序的性能。例如,编译器可能会把一个变量的值缓存到寄存器中,以避免频繁地读写内存。然而,在一些特定的场景中,变量的值可能会在程序的执行过程中被外部因素改变。
如果编译器对变量进行优化,可能会导致程序出现不可预测的错误。因此,使用volatile关键字可以告诉编译器不要对该变量进行优化,每次都要从内存中读取该变量的值。
2.2强制编译器按照程序顺序访问变量
在一些特定的场景中,程序的正确性依赖于变量的读写顺序。例如,在多线程程序中,多个线程可能会同时访问同一个变量。如果编译器对变量进行了优化,可能会导致不同线程看到的变量值不一致,从而引发程序错误。
使用 volatile 关键字可以强制编译器按照程序顺序访问变量。具体来说,编译器会按照程序中变量的访问顺序来访问变量,而不是按照优化后的顺序来访问变量。
2.3提供内存屏障
在一些特定的场景中,程序需要对变量的读写顺序进行严格的控制。例如,在多线程程序中,变量的读写顺序可能会影响程序的正确性。
为了保证程序的正确性,需要在变量的读写操作之间插入内存屏障(Memory Barrier),以确保变量的读写顺序符合程序的要求。
使用 volatile 关键字可以提供内存屏障,以确保变量的读写顺序符合程序的要求。具体来说,使用 volatile 关键字可以在变量的读写操作之间插入内存屏障,以确保变量的读写顺序符合程序的要求。
3.volatile关键字的注意事项
虽然 volatile 关键字可以防止编译器优化,强制编译器按照程序顺序访问变量,提供内存屏障,但是不应该滥用 volatile 关键字。因为 volatile 关键字会影响程序的性能,使用过多的 volatile 关键字会导致程序变慢。
3.1不要依赖volatile关键字解决多线程问题
虽然 volatile 关键字可以防止编译器优化,强制编译器按照程序顺序访问变量,但是不应该依赖 volatile 关键字解决多线程问题。因为 volatile 关键字无法保证原子性,不能解决多线程竞争的问题。解决多线程竞争的问题需要使用其他的同步机制,比如互斥锁、读写锁、信号量等。
3.2注意内存屏障的使用
在使用 volatile 关键字提供的内存屏障时,需要注意内存屏障的使用。具体来说,内存屏障应该放置在正确的位置,以确保变量的读写顺序符合程序的要求。如果内存屏障放置不当,可能会导致程序出现不可预测的错误。
3.3注意编译器的实现
不同编译器对 volatile 关键字的实现可能存在差异。因此,在使用 volatile 关键字时,需要注意编译器的实现。特别是在多平台开发中,不同平台上的编译器对 volatile 关键字的实现可能存在差异,需要谨慎处理。
二、不同的使用场景:
1.并行设备的硬件寄存器
存储器映射的硬件寄存器通常加volatile,因为寄存器随时可以被外设硬件修改。当声明指向设备寄存器的指针时一定要用volatile,它会告诉编译器不要对存储在这个地址的数据进行假设。也就是说每次都从对应的地址去取这个值。
XBYTE[2]=0x55;
XBYTE[2]=0x56;
XBYTE[2]=0x57;
XBYTE[2]=0x58;
对外部硬件而言,上述四条语句分别表示不同的操作,会产生四种不同的动作,但是编译器却会对上述四条语句进行优化,认为只有XBYTE[2]=0x58(即忽略前三条语句,只产生一条机器代码)。如果键入volatile,则编译器会逐一地进行编译并产生相应的机器代码(产生四条代码)。
2.一个中断服务程序中修改的供其他程序检测的变量
volatile提醒编译器,它后面所定义的变量随时都有可能改变。因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
3.多线程应用中
被几个任务共享单变量。因为对于C编译器来说,它并不知道这个值会被其他线程修改。自然就把它cache在寄存器里面。C 编译器是没有线程概念的。在这里volatile 的本意是指:这个值可能会在当前线程外部被改变。
在实现自旋锁等低层次的并发控制时,可以使用 volatile 来保证在忙等待期间检查锁的状态变量是最新的。
volatile int lock = 0;
void acquire_lock() {
while (__atomic_test_and_set(&lock, __ATOMIC_ACQUIRE)) {
// 自旋等待
}
}
void release_lock() {
__atomic_clear(&lock, __ATOMIC_RELEASE);
}