詳解如何使用C++寫一個(gè)線程安全的單例模式
單例模式的簡單實(shí)現(xiàn)
單例模式大概是流傳最為廣泛的設(shè)計(jì)模式之一了。一份簡單的實(shí)現(xiàn)代碼大概是下面這個(gè)樣子的:
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)境下是完全沒有問題的,但到了多線程的世界里,情況就有一點(diǎn)不同了。考慮以下執(zhí)行順序:
- 線程1執(zhí)行完if (inst_ != nullptr)之后,掛起了;
- 線程2執(zhí)行instance函數(shù):由于inst_還未被賦值,程序會(huì)inst_ = new singleton()語句;
- 線程1恢復(fù),inst_ = new singleton()語句再次被執(zhí)行,單例句柄被多次創(chuàng)建。
所以,這樣的實(shí)現(xiàn)是線程不安全的。
有問題的雙重檢測(cè)鎖
解決多線程的問題,最常用的方法就是加鎖唄。于是很容易就可以得到以下的實(shí)現(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)鍵的是,這個(gè)鎖也不是每次都需要啊!實(shí)際我們只有在創(chuàng)建單例實(shí)例的時(shí)候才需要加鎖,之后使用的時(shí)候是完全不需要鎖的。于是,有人提出了一種雙重檢測(cè)鎖的寫法:
... static singleton* instance() { if (inst_ != nullptr) { guard<mutex> lock{ mut_ }; if (inst_ != nullptr) { inst_ = new singleton(); } } return inst_; } ...
我們先判斷一下inst_是否已經(jīng)初始化了,如果沒有,再進(jìn)行加鎖初始化流程。這樣,雖然代碼看上去有點(diǎn)怪異,但好像確實(shí)達(dá)到了只在創(chuàng)建單例時(shí)才引入鎖開銷的目的。不過遺憾的是,這個(gè)方法是有問題的。Scott Meyers 和 Andrei Alexandrescu 兩位大神在C++ and the Perils of Double-Checked Locking 一文中對(duì)這個(gè)問題進(jìn)行了非常詳細(xì)地討論,我們?cè)谶@兒只作一個(gè)簡單的說明,問題出在:
inst_ = new singleton();
這一行。這句代碼不是原子的,它通常分為以下三步:
- 調(diào)用operator new為singleton對(duì)象分配內(nèi)存空間;
- 在分配好的內(nèi)存空間上調(diào)用singleton的構(gòu)造函數(shù);
- 將分配的內(nèi)存空間地址賦值給inst_。
如果程序能嚴(yán)格按照1-->2-->3的步驟執(zhí)行代碼,那么上述方法沒有問題,但實(shí)際情況并非如此。編譯器對(duì)指令的優(yōu)化重排、CPU指令的亂序執(zhí)行(具體示例可參考《【多線程那些事兒】多線程的執(zhí)行順序如你預(yù)期嗎?》)都有可能使步驟3執(zhí)行早于步驟2。考慮以下的執(zhí)行順序:
- 線程1按步驟1-->3-->2的順序執(zhí)行,且在執(zhí)行完步驟1,3之后被掛起了;
- 線程2執(zhí)行instance函數(shù)獲取單例句柄,進(jìn)行進(jìn)一步操作。
由于inst_在線程1中已經(jīng)被賦值,所以在線程2中可以獲取到一個(gè)非空的inst_實(shí)例,并繼續(xù)進(jìn)行操作。但實(shí)際上單例對(duì)像的創(chuàng)建還沒有完成,此時(shí)進(jìn)行任何的操作都是未定義的。
現(xiàn)代C++中的解決方法
在現(xiàn)代C++中,我們可以通過以下幾種方法來實(shí)現(xiàn)一個(gè)即線程安全、又高效的單例模式。
使用現(xiàn)代C++中的內(nèi)存順序限制
現(xiàn)代C++規(guī)定了6種內(nèi)存執(zhí)行順序。合理的利用內(nèi)存順序限制,即可避免代碼指令重排。一個(gè)可行的實(shí)現(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í)行順序。
使用現(xiàn)代C++中的call_once方法
call_once也是現(xiàn)代C++中引入的新特性,它可以保證某個(gè)函數(shù)只被執(zhí)行一次。使用call_once的代碼實(shí)現(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í)行一次。
使用靜態(tài)局部變量
現(xiàn)在C++對(duì)變量的初始化順序有如下規(guī)定:
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
所以我們可以簡單的使用一個(gè)靜態(tài)局部變量來實(shí)現(xiàn)線程安全的單例模式:
class singleton { public: static singleton* instance() { static singleton inst_; return &inst_; } private: singleton(){} };
來看一下匯編代碼:
可以看到,編譯器已經(jīng)自動(dòng)幫我們插入了相關(guān)的代碼,來保證靜態(tài)局部變量初始化的多線程安全性。
以上就是詳解如何使用C++寫一個(gè)線程安全的單例模式的詳細(xì)內(nèi)容,更多關(guān)于C++線程安全的單例模式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
深入C++實(shí)現(xiàn)函數(shù)itoa()的分析
本篇文章是對(duì)C++實(shí)現(xiàn)函數(shù)itoa()進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05Vs2022環(huán)境下安裝低版本.net framework的實(shí)現(xiàn)步驟
本文主要介紹了Vs2022環(huán)境下安裝低版本.net framework的實(shí)現(xiàn)步驟,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04C++實(shí)現(xiàn)無重復(fù)字符的最長子串
本文主要介紹了C++實(shí)現(xiàn)無重復(fù)字符的最長子串,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07C++教程之a(chǎn)rray數(shù)組使用示例詳解
這篇文章主要為大家介紹了C++教程之a(chǎn)rray數(shù)組使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03C++/Php/Python/Shell 程序按行讀取文件或者控制臺(tái)的實(shí)現(xiàn)
下面小編就為大家?guī)硪黄狢++/Php/Python/Shell 程序按行讀取文件或者控制臺(tái)的實(shí)現(xiàn)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-03-03