volatile的作用
保存内存的可见性。告诉编译器,volatile修饰的变量,不允许优化,对该变量的任何操作都必须再真实的内存中进行操作。
volatile应用场景
懒汉方式-单例模式
懒汉方式中的静态对象指针要加volatile关键字防止过度优化出现问题。
template <typename T>
class Singleton {
volatile static T* inst;
static std::mutex lock;
public:
static T* GetInstance() {
if (inst == NULL) {
lock.lock();
if (inst == NULL) {
inst = new T();
}
lock.unlock();
}
return inst;
}
};
new 底层调用为operator new 和构造函数 ,operator new 底层调用malloc ,失败了抛异常。当调用结束后返回内存地址赋值给inst。
inst = new T();
而问题处在于初始化和赋值地址会出现重排序(重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段)既可能会出现重排序从申请空间,构造函数初始化后再赋值地址排序变为申请空间,赋值地址后再构造函数初始化,那么在多线程的情况下就会导致出现问题。
线程A执行到new(),开始初始化实例对象,由于存在指令重排序,这次new操作先执行了3把地址赋值了,还没有执行2初始化实例。这时时间片结束了,切换到线程B执行,线程B调用new T()后发现地址不等于null,便直接返回引用地址了,这样会导致线程B访问到了一个还未初始化的对象。
捕捉信号修改全局变量
#include<iostream>
#include<cstdio>
#include<signal.h>
using namespace std;
int quit = 0;
void handler(int sig) {
printf("set quit = 1\n");
quit = 1;
}
int main() {
signal(2, handler);
while(!quit);
return 0;
}
当我们在函数中使用一些不会修改的变量时,编译器可以对其进行优化。因为数据不会改变,我们可以让CPU在运算时,只要在寄存器里拿数据就好了,就不用到内存拿数据,效率会高很多。因此此时编译器认为main函数中的quit关键字一直没变,存在寄存器中。而handler()中存的变量在内存中,改变对寄存器中该变量不会产生影响。
针对这种情况我们对该全局变量加volatile 关键字即可。
#include<iostream>
#include<cstdio>
#include<signal.h>
using namespace std;
volatile int quit = 0;
void handler(int sig) {
printf("set quit = 1\n");
quit = 1;
}
int main() {
signal(2, handler);
while(!quit);
return 0;
}
此时正常结束。
总结
总结:volatile修饰的变量,不允许编译器优化,对该变量的任何操作都必须在真实的内存中进行操作。
|