C++单例模式与线程安全
最简单的单例模式可以是
// single thread safe version
class Singleton {
public:
static Singleton* GetInstance(int x = 0) {
if (instance_ == NULL) {
instance_ = new Singleton(x);
}
return instance_;
}
void Print() {
std::cout << this->member_ << std::endl;
}
private:
Singleton(int x = 3) : member_(x) {}
int member_;
static Singleton* instance_; //declare a static member variable
};
Singleton* Singleton::instance_ = NULL; //define a static member variable
然而,在多线程情况下,比如线程A 判断instance_
为空后,此时线程A暂停执行挂起,而线程B 判断 instance_
也为空,于是会调用构造函数创建对象,并返回,而线程A继续恢复运行后,同样会调用构造函数创建对象,并修改 instance_
指针。导致第一个指针被修改,引发线程安全问题。
基于此,可以在判断 instance_
为空前,先加锁,拿到锁的线程才能进一步判断 instance_
是否为空,并进一步决定是否创建该对象。代码如下
// thread safe, but inefficiency
class Singleton {
public :
static Singleton* GetInstance(int x = 0) {
std::lock_guard<std::mutex> lock(mtx);
if (instance_ == NULL) {
instance_ = new Singleton(x);
}
return instance_;
}
void Print() {
std::cout << this->member_ << std::endl;
}
private:
Singleton(int x = 3) : member_(x) {}
int member_;
static Singleton* instance_; //declare a static member variable
};
Singleton* Singleton::instance_ = NULL; //define a static member variable
然而,此方法存在效率问题,每次调用 GetInstance()
获取该对象指针时,都需要加锁。显然,我们只是希望在首次创建对象的时候才需要加锁,后续的访问不用再加锁(已经创建好了对象),于是很自然的想到在加锁前,再进行一次判断 instance_
是否为空,代码如下
// double-checked locking
class Singleton {
public :
static Singleton* GetInstance(int x = 0) {
if (instance_ == NULL) {
std::lock_guard<std::mutex> lock(mtx);
if (instance_ == NULL) {
instance_ = new Singleton(x);
/*
instance_ = // point to memory
operator new(sizeof(Singleton)); // memory allocate
new (instance_) Singleton; // construct a object int the allocated memory, may occur exception
*/
}
}
return instance_b_;
}
void Print() {
std::cout << this->member_ << std::endl;
}
private:
Singleton(int x = 3) : member_(x) {}
int member_;
static Singleton* instance_; //declare a static member variable
};
Singleton* Singleton::instance_ = NULL; //define a static member variable
上述方法很好的解决了频繁加锁带来的开销,但是仍然存在一些问题,对于 new
调用,其过程为
- 从堆区分配内存
- 在分配的内存执行构造函数
- 返回其指针
而实际上,经过编译器优化后的代码执行顺序可能并不是这样的,比如
instance_ = new Singleton(x);
可能等价于
instance_ = operator new(sizeof(Singleton)); // memory allocate
new (instance_) Singleton; // placement new
即
- 从堆区分配内存
- 返回堆区的指针
- 对指针指向的内存执行构造函数
该情形下,如果线程A在执行到 instance_ = operator new(sizeof(Singleton));
后,暂停运行挂起,线程B在进入GetInstance()
并判断 instance_
是否为空时,发现其不为空,于是返回 instance_
指针,然而该指针是没有经过初始化的,所以线程B对该指针的使用将可能会引起未定义的行为。为了防止发生编译器的优化后的代码执行顺序和我们预期的不一致(上述2,3步调换了顺序),我们希望确保在 new
调用构造函数成功后,再修改 instance_
指针,代码如下
// in-case exception occur when construct
class Singleton {
public :
static Singleton* GetInstance(int x = 0) {
if (instance_ == NULL) {
std::lock_guard<std::mutex> lock(mtx);
if (instance_ == NULL) {
Singleton* temp = new Singleton(x);
instance_ = temp;
}
}
return instance_;
}
void Print() {
std::cout << this->member_ << std::endl;
}
private:
Singleton(int x = 3) : member_(x) {}
int member_;
static Singleton* instance_; //declare a static member variable
};
Singleton* Singleton::instance_ = NULL; //define a static member variable
这里我们引入了 temp
指针,用于确保 new
调用成功后,再去修改 instance_
指针,对new
进行替换后如下
// in-case exception occur when construct
class Singleton {
public :
static Singleton* GetInstance(int x = 0) {
if (instance_ == NULL) {
std::lock_guard<std::mutex> lock(mtx);
if (instance_ == NULL) {
Singleton* temp = operator new(sizeof(Singleton)); // memory allocate
new (temp) Singleton; // placement new
instance_ = temp;
}
}
return instance_;
}
void Print() {
std::cout << this->member_ << std::endl;
}
private:
Singleton(int x = 3) : member_(x) {}
int member_;
static Singleton* instance_; //declare a static member variable
};
Singleton* Singleton::instance_ = NULL; //define a static member variable
然而,对于上述代码,编译器仍可能对进行优化,编译器会发现temp 变量在程序中只是起到一个传递值的作用,可以被优化掉,优化后的代码将直接是 instance_ = new Singleton(x);
,为了防止编译器出现此类优化,我们可以进一步使用 volatile
关键字来防止编译器的优化,代码如下
class Singleton {
public :
static Singleton* GetInstance(int x = 0) {
if (instance_ == NULL) {
std::lock_guard<std::mutex> lock(mtx);
if (instance_ == NULL) {
Singleton* volatile temp = new Singleton(x);
instance_ = temp;
}
}
return instance_;
}
void Print() {
std::cout << this->member_ << std::endl;
}
private:
Singleton(int x = 3) : member_(x) {}
int member_;
static Singleton* instance_; //declare a static member variable
};
Singleton* Singleton::instance_ = NULL; //define a static member variable
上述代码对temp
变量声明为 volatile
,其目的在于告诉编译器,temp
相关的代码块不能进行优化,temp
相关的指令序列和高级语言看到的应该完全一致。
然而,这里还是可能存在问题,volatile
只保证了对temp变量的相关代码的操作顺序,而对temp
的成员没有保证,比如下述代码
// in-case exception occur when construct
class Singleton {
public :
static Singleton* GetInstance(int x = 0) {
if (instance_ == NULL) {
std::lock_guard<std::mutex> lock(mtx);
if (instance_ == NULL) {
Singleton* temp = operator new(sizeof(Singleton)); // memory allocate
temp->member_ = x; // construct
instance_ = temp;
}
}
return instance_;
}
void Print() {
std::cout << this->member_ << std::endl;
}
private:
Singleton(int x = 3) : member_(x) {}
int member_;
static Singleton* instance_; //declare a static member variable
};
Singleton* Singleton::instance_ = NULL; //define a static member variable
temp的构造函数在对temp 的成员变量进行赋值时 temp->member_ = x
,该操作和 instance_ = temp
可能会因为编译器的优化而重排顺序,如果 instance_ = temp
先于temp->member_ = x
执行
instance_ = temp;
temp->member_ = x
当线程A 在执行完 instance_ = temp;
后暂时挂起,线程B获取到 instance_
后访问其成员变量 member_
时,将会引发未定义的行为(如果是指针,将会发生core),因此我们需要保证 类Singleton
的所有成员变量的相关操作应该也是 volatile
的,于是代码如下
class Singleton {
public :
static volatile Singleton* GetInstance(int x = 0) {
if (instance_ == NULL) {
std::lock_guard<std::mutex> lock(mtx);
if (instance_ == NULL) {
volatile Singleton* temp = new volatile Singleton(x);
instance_ = temp;
}
}
return instance_;
}
void Print() {
std::cout << this->member_ << std::endl;
}
private:
Singleton(int x = 3) : member_(x) {}
int member_;
static volatile Singleton* instance_; //declare a static member variable
};
volatile Singleton* Singleton::instance_ = NULL; //define a static member variable
参考链接:https://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf