許可權類 主要用途:用戶登錄了,某個介面可能只有超級管理員才能訪問,普通用戶不能訪問 案列:出版社的所有介面,必須登錄,而且是超級管理員才能訪問 分析步驟 第一步:寫一個類,繼承BasePermission 第二步:重寫has_permission方法 第三步:在方法校驗用戶時候有許可權(request ...
如何寫一個線程安全的單例模式?
單例模式的簡單實現
單例模式大概是流傳最為廣泛的設計模式之一了。一份簡單的實現代碼大概是下麵這個樣子的:
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(){}
};
來看一下彙編代碼:
可以看到,編譯器已經自動幫我們插入了相關的代碼,來保證靜態局部變數初始化的多線程安全性。
全文完。