系列文章目录
文章目录
C++面试基础系列-volatile
1.volatile核心规则
- volatile修饰变量或指针功能
- 告诉编译器,被volatile修饰的变量或指针(寄存器或硬件寄存器),不要进行优化
- 变量或指针在中断服务子程序,用户函数中同时调用,不要进行优化
- 多线程,多任务同时调用变量或指针,使用volatile修饰,表示每次操作该变量或指针,都要从内存中取最新的值进行操作。
- 简单来说
- 编译器,不要优化
- 从内存取值
2.C与C++中volatile区别
- 在C和C++中,volatile关键字都用于告诉编译器一个变量可能会在程序的控制之外被改变,通常是由于硬件的作用。尽管C和C++都支持volatile,但它们在某些方面存在一些差异:
2.1.C语言中的volatile
- 在C语言中,volatile主要用于多线程环境或中断服务例程(ISR)中,指示编译器该变量可能会在任何时候被改变,因此每次使用时都应该从内存中重新读取,而不是从寄存器中。
- C标准并没有详细说明volatile的所有行为,尤其是在多线程同步方面。
2.2.C++中的volatile
- C++中的volatile用法与C类似,但C++标准对volatile的行为有更明确的定义,特别是在多线程环境中。
- 在C++11及以后的版本中,标准定义了内存模型,volatile变量的访问将遵循这些规则,以确保在多线程环境中对它们的访问是原子的(如果需要)。
2.3.原子性和顺序
- 在C++11中,volatile关键字不保证操作的原子性,即使在多线程环境中也是如此。如果需要原子性,C++提供了std::atomic模板和相关的原子操作函数。
- C++中的volatile也不保证编译器不会重排相关操作的顺序,这与C类似。
2.4.易失性
- 在C和C++中,volatile都可以用来指示编译器一个变量是易失性的,即它可能会因为外部硬件事件而改变。
2.5.优化
- 由于volatile告诉编译器变量可能在任何时候改变,编译器将不会对这个变量进行优化。
2.6.使用场景
- 在嵌入式编程中,volatile经常用于访问内存映射的硬件寄存器,因为这些寄存器可能会被外部硬件改变。
2.7.C++特有的特性
- C++中volatile的使用可能会受到C++特有的特性影响,如类和对象模型。例如,C++中的volatile成员变量需要特别注意,因为它们的行为可能不如预期。
2.8.C++20引入的变化(如果有)
- C++20标准可能会对volatile有进一步的说明或改进,但截至知识截止日期,这些变化尚未明确。
总结来说,C和C++中的volatile在基本用途上相似,但C++标准提供了更明确的定义,尤其是在多线程环境中。在C++中,如果需要原子性或线程安全的同步,应该使用std::atomic或其他同步机制,而不是仅仅依赖volatile。
3.volatile常见面试问题
在面试中,volatile
关键字是一个常见的话题,以下是一些可能会问到的问题以及相关的答案:
volatile
关键字是什么?
volatile
是一种类型修饰符,用于告诉编译器该变量可能会在程序的控制之外被改变,例如由中断服务程序、多线程或硬件修改。使用volatile
可以防止编译器对这些变量进行优化,确保每次访问都直接从内存中读取或写入,从而保证数据的一致性。volatile
变量的可见性如何保证?
volatile
变量保证了对所有线程的可见性。当一个线程修改了一个volatile
变量时,其他线程能够立即看到这个修改,这是因为volatile
变量的写操作会立即刷新到主内存中,而读操作会从主内存中读取最新值。volatile
能否保证原子性?
volatile
关键字不能保证操作的原子性。它只能确保单次的读/写操作具有原子性,但对于复合操作,如自增(i++
),volatile
无法保证整个操作的原子性。因此,对于需要原子性保证的操作,应该使用其他同步机制,如synchronized
或AtomicInteger
。volatile
变量在多线程中的使用场景有哪些?
volatile
变量适用于多线程环境下的某些特定场景,例如:- 作为状态标志,用于控制线程间的协调,如退出循环的条件。
- 在没有其他同步机制的情况下,用于保护简单的状态变量,以确保变量的可见性和有序性。
- 与锁结合使用,实现读写锁模式,提高性能。
volatile
与const
的区别是什么?
const
关键字用于定义常量,即值不能被修改的变量。而volatile
用于定义可能会在程序的控制之外被改变的变量。const
保证的是值的不变性,而volatile
保证的是变量的可见性和禁止指令重排序。volatile
指针是什么?
volatile
指针是指指向的变量可能会在程序的控制之外被改变的指针。它可以是指向volatile
变量的指针,也可以是指针本身的值是volatile
的。在使用volatile
指针时,编译器不会对通过该指针进行的操作进行优化。- 在嵌入式系统中,
volatile
的作用是什么?
在嵌入式系统中,volatile
通常用于声明与硬件寄存器相关的变量,以确保编译器不会对与硬件交互的代码进行优化。这是因为硬件寄存器的值可能会由硬件本身的操作所改变,需要每次访问时都从内存中读取最新值。 volatile
能否替代synchronized
?
volatile
在某些情况下可以作为synchronized
的轻量级替代,特别是在读操作远多于写操作的场景下。但是,由于volatile
不能保证复合操作的原子性,它不能完全替代synchronized
。在需要原子性保证的情况下,仍然需要使用synchronized
或其他同步机制。
理解 volatile
的作用和限制对于编写正确的多线程程序至关重要。在面试中,展示对 volatile
的深入理解可以体现候选人的专业知识和经验。
4.volatile与const与指针
- (1)一个参数既可以是const还可以是volatile吗?为什么?
可以。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。 - (2)一个指针可以是volatile吗?为什么?
可以。尽管这并不常见。一个例子是当一个中断服务子程序修改一个指向一个buffer的指针时。
5.应用场景
在嵌入式系统开发中,正确地使用 volatile 关键字对于避免硬件中断对程序执行的影响至关重要。以下是一些关于如何在嵌入式系统中使用 volatile 的关键点:
- 防止编译器优化:volatile 告诉编译器,即使在代码中看似没有改变,变量的值也可能在任何时候改变,因此编译器不应进行优化
- 中断服务程序中的变量:在中断服务程序(ISR)中,经常需要访问或修改一些变量。如果这些变量在主程序中也被访问,那么它们应该被声明为 volatile,以确保每次访问时都能获取最新的值
- 多线程共享变量:在多线程环境中,如果多个任务共享某些变量,并且这些变量的值可能被任何一个任务改变,那么这些变量也应该被声明为 volatile,以确保所有任务都能看到其他任务对共享变量的最新修改
- 硬件寄存器访问:在嵌入式编程中,硬件寄存器的值可能会被硬件本身的操作所改变。使用 volatile 修饰硬件寄存器可以确保每次访问都是直接从硬件寄存器中读取,而不是从CPU缓存中
- 保证内存顺序:volatile 还可以防止编译器和处理器对指令的重排序,确保指令按照代码中的顺序执行,这对于中断和主程序之间的同步尤为重要
- 使用场景:volatile 适用于并行设备的硬件寄存器、中断服务子程序中访问的非自动变量、多线程应用中被多个任务共享的变量,以及需要防止编译器优化的情况,如for循环延时程序
- 注意限制:虽然 volatile 可以确保变量的可见性,但它不保证操作的原子性。在多线程环境中,如果需要原子性,还需要使用其他同步机制,如互斥锁
- 性能影响:使用 volatile 可能会降低程序的性能,因为它阻止了编译器进行某些优化。因此,只有在必要时才应该使用 volatile
总结来说,volatile 在嵌入式系统中是一个关键的关键字,用于确保变量的值能够反映出最新的状态,特别是在中断服务程序和多线程环境中。然而,开发者应当谨慎使用,避免滥用,同时注意它并不能替代其他同步机制来保证操作的原子性。
6.应用示例
(1)并行设备的硬件寄存器(如状态寄存器)。
- 假设要对一个设备进行初始化,此设备的寄存器为0x0x80008004。
unsigned int *output = (unsigned int *)0x0x80008004; //定义一个IO端口;
int main(void)
{
int i;
for(i=0;i< 10;i++)
{
*output = i;
}
}
- 如果开启的 -O3 优化,那么经过编译器优化后,编译器认为前面循环,对最后的结果毫无影响。
- 最终只是将output这个指针赋值为 9,所以汇编后的程序相当于:
int init(void)
{
*output = 9;
}
- 如果你需要程序完全按照你所写程序运行,那就用volatile修饰变量。
- 通知编译器这个变量是一个不稳定的,在遇到此变量时候不要优化。
(2)一个中断服务子程序中访问到的变量;
static int i=0;
int main()
{
while(1)
{
if(i) dosomething();
}
}
/* Interrupt service routine */
void IRS()
{
i=1;
}
- 上面示例程序的本意是产生中断时,由中断服务子程序IRS响应中断,变更程序变量i,使在main函数中调用dosomething函数,
- 但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远不会被调用。
- 如果将变量i加上volatile修饰,则编译器保证对变量i的读写操作都不会被优化,从而保证了变量i被外部程序更改后能及时在原程序中得到感知。
(3)多线程应用中被多个任务共享的变量。
- 当多个线程共享某一个变量时,该变量的值会被某一个线程更改,应该用 volatile 声明。
- 作用是防止编译器优化把变量从内存装入CPU寄存器中,当一个线程更改变量后,未及时同步到其它线程中导致程序出错。
- volatile的作用是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。示例如下:
volatile bool bStop=false; //bStop 为共享全局变量
//第一个线程
void threadFunc1()
{
...
while(!bStop){...}
}
//第二个线程终止上面的线程循环
void threadFunc2()
{
...
bStop = true;
}
- 要想通过第二个线程终止第一个线程循环,如果bStop不使用volatile定义,那么这个循环将是一个死循环,因为bStop已经读取到了寄存器中,寄存器中bStop的值永远不会变成FALSE,
- 加上volatile,程序在执行时,每次均从内存中读出bStop的值,就不会死循环了。
关于作者
- 本文作者:WeSiGJ
- 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
- GitHub:https://github.com/wesigj/CPLUSCPLUSBOYS
- CSDN:https://blog.csdn.net/wesigj
- 微博:
- 微信公众号:WeSiGJ