如何寫一個執行緒安全的單例模式?
單例模式的簡單實作
單例模式大概是流傳最為廣泛的設計模式之一了,一份簡單的實作代碼大概是下面這個樣子的:
class singleton
{
public:
static singleton* instance()
{
if (inst_ != nullptr) {
inst_ = new singleton();
}
return inst_;
}
private:
singleton(){}
static singleton* inst_;
};
singleton* singleton::inst_ = nullptr;
這份代碼在單執行緒的環境下是完全沒有問題的,但到了多執行緒的世界里,情況就有一點不同了,考慮以下執行順序:
- 執行緒1執行完if (inst_ != nullptr)之后,掛起了;
- 執行緒2執行instance函式:由于inst_還未被賦值,程式會inst_ = new singleton()陳述句;
- 執行緒1恢復,inst_ = new singleton()陳述句再次被執行,單例句柄被多次創建,
所以,這樣的實作是執行緒不安全的,
有問題的雙重檢測鎖
解決多執行緒的問題,最常用的方法就是加鎖唄,于是很容易就可以得到以下的實作版本:
class singleton
{
public:
static singleton* instance()
{
guard<mutex> lock{ mut_ };
if (inst_ != nullptr) {
inst_ = new singleton();
}
return inst_;
}
private:
singleton(){}
static singleton* inst_;
static mutex mut_;
};
singleton* singleton::inst_ = nullptr;
mutex singleton::mut_;
這樣問題是解決了,但性能上就不那么另人滿意,畢竟每一次使用instance都多了一次加鎖和解鎖的開銷,更關鍵的是,這個鎖也不是每次都需要啊!實際我們只有在創建單例實體的時候才需要加鎖,之后使用的時候是完全不需要鎖的,于是,有人提出了一種雙重檢測鎖的寫法:
...
static singleton* instance()
{
if (inst_ != nullptr) {
guard<mutex> lock{ mut_ };
if (inst_ != nullptr) {
inst_ = new singleton();
}
}
return inst_;
}
...
我們先判斷一下inst_是否已經初始化了,如果沒有,再進行加鎖初始化流程,這樣,雖然代碼看上去有點怪異,但好像確實達到了只在創建單例時才引入鎖開銷的目的,不過遺憾的是,這個方法是有問題的,Scott Meyers 和 Andrei Alexandrescu 兩位大神在C++ and the Perils of Double-Checked Locking 一文中對這個問題進行了非常詳細地討論,我們在這兒只作一個簡單的說明,問題出在:
inst_ = new singleton();
這一行,這句代碼不是原子的,它通常分為以下三步:
- 呼叫operator new為singleton物件分配記憶體空間;
- 在分配好的記憶體空間上呼叫singleton的建構式;
- 將分配的記憶體空間地址賦值給inst_,
如果程式能嚴格按照1-->2-->3的步驟執行代碼,那么上述方法沒有問題,但實際情況并非如此,編譯器對指令的優化重排、CPU指令的亂序執行(具體示例可參考《【多執行緒那些事兒】多執行緒的執行順序如你預期嗎?》)都有可能使步驟3執行早于步驟2,考慮以下的執行順序:
- 執行緒1按步驟1-->3-->2的順序執行,且在執行完步驟1,3之后被掛起了;
- 執行緒2執行instance函式獲取單例句柄,進行進一步操作,
由于inst_在執行緒1中已經被賦值,所以在執行緒2中可以獲取到一個非空的inst_實體,并繼續進行操作,但實際上單例對像的創建還沒有完成,此時進行任何的操作都是未定義的,
現代C++中的解決方法
在現代C++中,我們可以通過以下幾種方法來實作一個即執行緒安全、又高效的單例模式,
使用現代C++中的記憶體順序限制
現代C++規定了6種記憶體執行順序,合理的利用記憶體順序限制,即可避免代碼指令重排,一個可行的實作如下:
class singleton {
public:
static singleton* instance()
{
singleton* ptr = inst_.load(memory_order_acquire);
if (ptr == nullptr) {
lock_guard<mutex> lock{ mut_ };
ptr = inst_.load(memory_order_relaxed);
if (ptr == nullptr) {
ptr = new singleton();
inst_.store(ptr, memory_order_release);
}
}
return inst_;
}
private:
singleton(){};
static mutex mut_;
static atomic<singleton*> inst_;
};
mutex singleton::mut_;
atomic<singleton*> singleton::inst_;
來看一下匯編代碼:

可以看到,編譯器幫我們插入了必要的陳述句來保證指令的執行順序,
使用現代C++中的call_once方法
call_once也是現代C++中引入的新特性,它可以保證某個函式只被執行一次,使用call_once的代碼實作如下:
class singleton
{
public:
static singleton* instance()
{
if (inst_ != nullptr) {
call_once(flag_, create_instance);
}
return inst_;
}
private:
singleton(){}
static void create_instance()
{
inst_ = new singleton();
}
static singleton* inst_;
static once_flag flag_;
};
singleton* singleton::inst_ = nullptr;
once_flag singleton::flag_;
來看一下匯編代碼:

可以看到,程式最終呼叫了__gthrw_pthread_once來保證函式只被執行一次,
使用靜態區域變數
現在C++對變數的初始化順序有如下規定:
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
所以我們可以簡單的使用一個靜態區域變數來實作執行緒安全的單例模式:
class singleton
{
public:
static singleton* instance()
{
static singleton inst_;
return &inst_;
}
private:
singleton(){}
};
來看一下匯編代碼:

可以看到,編譯器已經自動幫我們插入了相關的代碼,來保證靜態區域變數初始化的多執行緒安全性,
全文完,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/518718.html
標籤:其他
上一篇:權限類與頻率類
