單例模式大概是流傳最為廣泛的設(shè)計模式之一了。一份簡單的實現(xiàn)代碼大概是下面這個樣子的:
網(wǎng)站建設(shè)哪家好,找創(chuàng)新互聯(lián)公司!專注于網(wǎng)頁設(shè)計、網(wǎng)站建設(shè)、微信開發(fā)、小程序制作、集團企業(yè)網(wǎng)站建設(shè)等服務(wù)項目。為回饋新老客戶創(chuàng)新互聯(lián)還提供了定南免費建站歡迎大家使用!
class singleton
{
public:
static singleton* instance()
{
if (inst_ != nullptr) {
inst_ = new singleton();
}
return inst_;
}
private:
singleton(){}
static singleton* inst_;
};
singleton* singleton::inst_ = nullptr;
這份代碼在單線程的環(huán)境下是完全沒有問題的,但到了多線程的世界里,情況就有一點不同了??紤]以下執(zhí)行順序:
所以,這樣的實現(xiàn)是線程不安全的。
解決多線程的問題,最常用的方法就是加鎖唄。于是很容易就可以得到以下的實現(xiàn)版本:
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都多了一次加鎖和解鎖的開銷。更關(guān)鍵的是,這個鎖也不是每次都需要啊!實際我們只有在創(chuàng)建單例實例的時候才需要加鎖,之后使用的時候是完全不需要鎖的。于是,有人提出了一種雙重檢測鎖的寫法:
...
static singleton* instance()
{
if (inst_ != nullptr) {
guard<mutex> lock{ mut_ };
if (inst_ != nullptr) {
inst_ = new singleton();
}
}
return inst_;
}
...
我們先判斷一下inst_是否已經(jīng)初始化了,如果沒有,再進行加鎖初始化流程。這樣,雖然代碼看上去有點怪異,但好像確實達到了只在創(chuàng)建單例時才引入鎖開銷的目的。不過遺憾的是,這個方法是有問題的。Scott Meyers 和 Andrei Alexandrescu 兩位大神在C++ and the Perils of Double-Checked Locking 一文中對這個問題進行了非常詳細地討論,我們在這兒只作一個簡單的說明,問題出在:
inst_ = new singleton();
這一行。這句代碼不是原子的,它通常分為以下三步:
如果程序能嚴格按照1-->2-->3的步驟執(zhí)行代碼,那么上述方法沒有問題,但實際情況并非如此。編譯器對指令的優(yōu)化重排、CPU指令的亂序執(zhí)行(具體示例可參考《【多線程那些事兒】多線程的執(zhí)行順序如你預期嗎?》)都有可能使步驟3執(zhí)行早于步驟2??紤]以下的執(zhí)行順序:
由于inst_在線程1中已經(jīng)被賦值,所以在線程2中可以獲取到一個非空的inst_實例,并繼續(xù)進行操作。但實際上單例對像的創(chuàng)建還沒有完成,此時進行任何的操作都是未定義的。
在現(xiàn)代C++中,我們可以通過以下幾種方法來實現(xiàn)一個即線程安全、又高效的單例模式。
現(xiàn)代C++規(guī)定了6種內(nèi)存執(zhí)行順序。合理的利用內(nèi)存順序限制,即可避免代碼指令重排。一個可行的實現(xiàn)如下:
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_;
來看一下匯編代碼:
可以看到,編譯器幫我們插入了必要的語句來保證指令的執(zhí)行順序。
call_once也是現(xiàn)代C++中引入的新特性,它可以保證某個函數(shù)只被執(zhí)行一次。使用call_once的代碼實現(xiàn)如下:
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_;
來看一下匯編代碼:
可以看到,程序最終調(diào)用了__gthrw_pthread_once來保證函數(shù)只被執(zhí)行一次。
現(xiàn)在C++對變量的初始化順序有如下規(guī)定:
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
所以我們可以簡單的使用一個靜態(tài)局部變量來實現(xiàn)線程安全的單例模式:
class singleton
{
public:
static singleton* instance()
{
static singleton inst_;
return &inst_;
}
private:
singleton(){}
};
來看一下匯編代碼:
可以看到,編譯器已經(jīng)自動幫我們插入了相關(guān)的代碼,來保證靜態(tài)局部變量初始化的多線程安全性。
全文完。
名稱欄目:【多線程那些事兒】如何使用C++寫一個線程安全的單例模式?
URL地址:http://chinadenli.net/article34/dsoiepe.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供Google、ChatGPT、品牌網(wǎng)站建設(shè)、軟件開發(fā)、網(wǎng)站營銷、品牌網(wǎng)站制作
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)