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