上一篇的日志類的實現裡有個這:
class Singleton<CLoggerImpl>
看名字便知其意——單例。這是一個單例模板類。
一個進程通常只有一個日志類實例,這很適合使用單例模式。那麼如何設計一個好的單例呢?
通常我們在網上看到有這樣的實現:
class SingletonAA { public: static SingletonAA& get_instance_ref() { static SingletonAA _inst; return _inst; } private: SingletonAA() { //... } ~SingletonAA() { //... } };
在首次調用函數get_instance_ref時,構造一個靜態實例,我以前也一直用的這種方式,後來看到一些討論單例的文章,才知道這種實現是有問題的:C++11之前的C++標准並沒有指明局部靜態變量初始化的線程安全性。就是說,這個靜態變量可能被兩個線程同時初始化或一個線程初始化了一部分,另一個線程又開始從頭初始化。
為了保證線程安全,有的同學可能使用這種方式:
class SingletonAA { public: static SingletonAA& get_instance_ref() { if (NULL == p_) { p_ = new SingletonAA(); } return *p_; } private: SingletonAA() { //... } ~SingletonAA() { //... } private: static SingletonAA *p_; }; SingletonAA *SingletonAA::p_ = NULL;
使用指針,在new之前判斷一下指針是否為空。然而這還是有問題:兩個線程可能都認為指針為空,然後都去new。
於是有了Double-Checked Locking Pattern (DCLP):
static SingletonAA& get_instance_ref() { if (NULL == p_) // 1st check { scoped_lock lock; if (NULL == p_) // 2nd check { p_ = new SingletonAA(); } } return *p_; }
做兩次判斷。因為new是在鎖內的,所以不用擔心多個線程同時new;進到鎖內部之後,又做了一次判斷,保證沒有別的線程在“第一次判斷”和“上鎖”這兩個動作的間隙new。
這“基本”上已經線程安全了。
但是——嗯,就是有“但是”——這個在C++中還是不對,問題出在這一句:
p_ = new SingletonAA();
不要看這只是一句代碼,實際上有三個動作:
1. 分配sizeof(SingletonAA)大小的內存
2. 在分配的這塊內存中構造一個SingletonAA對象
3. 使p_指向這塊內存
C++並沒有規定這三個步驟的執行順序,但是你也可以想到,第一個步驟肯定是首先執行的。“實踐”(來自文末DCLP參考文獻)中發現,編譯器可能會交換第二步和第三步的執行順序。我們設想一下,第三步在第二步之前執行的情況:
分配內存
指針賦值
構造對象
如果“指針賦值”之後,這個線程的時間片剛好用完了,另一個線程恰巧又走到“1st check”,發現指針不為空,那就直接開始使用這個未經初始化的對象了!
也許你可能會吐槽,為啥編譯器要把“指針賦值”放在“構造對象”之前,“too naïve,我的世界你不懂”編譯器君如是回道。若要一探究竟,請閱讀文末DCLP參考文獻。
照這樣說,如果我們把
p_ = new SingletonAA();
這句代碼和判斷條件分離開就行了,那麼這樣做:
class SingletonAA { public: static SingletonAA& get_instance_ref() { if (!init_flag_) // 1st check { scoped_lock lock; if (!init_flag_) // 2nd check { p_ = new SingletonAA(); init_flag_ = true; } } return *p_; } private: //... private: static SingletonAA *p_; static bool init_flag_; }; SingletonAA *SingletonAA::p_ = NULL; bool SingletonAA::init_flag_ = false;
bool在vc2008裡是一個字節的,讀寫只需一條匯編指令(其他版本的vc和g++我都沒試),是原子操作,不用考慮線程安全性。
貌似這樣就可以了,但是——嗯,又有但是——在get_instance_ref函數中,編譯器可能會對init_flag_的讀取進行優化:編譯器發現你並沒有在函數內部對init_flag_賦值,所以實際上它可能僅從變量地址中讀取一次,然後放在寄存器中,“2nd check”時從寄存器取,而不從變量地址中取,那麼你這兩次check的結果就永遠是一樣的了。
當然,我們是有辦法解決這個編譯器優化的問題的,想必你知道有個關鍵字volatile,它的作用就是告訴編譯器這個變量是隨時會變化的,請不要緩存它的值,每次都從地址中取,我們需要將init_flag_聲明成volatile的:
static bool volatile init_flag_;
這樣初始化:
bool volatile SingletonAA::init_flag_ = false;
看起來這樣就好了,然而——這次不用但是了,換個口味——這還是有問題的。具體是什麼問題請參閱文末DCLP參考文獻(這裡是我的todo)。
總之,單例無法用DCLP搞定。
那怎麼辦?
此時該祭出大殺器call_once了。
call_once,顧名思義,就是僅調用一次。這個東西有個參數是函數對象,它的作用就是保證你給他傳遞的函數對象只被執行一次,若在執行過程中又有線程過來了,則必須等待執行完畢並以其執行結果為自己的結果。
boost中有對應實現boost::call_once,C++11已經將它納入標准成為了std::call_once。
我的終極解決方案就用它了:
class SingletonAA { public: static inline SingletonAA& get_instance_ref() { boost::call_once(once_, init); return *p_; } private: //... static void init() { p_ = new SingletonAA(); } private: static SingletonAA *p_; static boost::once_flag once_; }; SingletonAA *SingletonAA::p_ = NULL; boost::once_flag SingletonAA::once_;
有興趣的同學可以看看boost::call_once是怎麼實現的(這裡是另一個todo)。
這裡邊還有最後一個問題:資源釋放。
我們new了一個對象,卻沒有delete。
一種辦法是顯式提供一個銷毀函數,這樣銷毀就必須由調用者保證,不太好;另外的辦法就是使用智能指針。
由於項目中好多地方都可能會用到單例,所以為了做的通用一點,我就把單例的實現做成了一個基類,子類不必再去寫get_instance_ref之類的代碼:
Show you my code:
template<typename Type> class Singleton : public boost::noncopyable { public: static Type& get_instance_ref() { boost::call_once(once_, init); return *(p_.get()); } protected: Singleton(){} ~Singleton(){} private: static void init() { p_.reset(new Type()); } private: typedef boost::shared_ptr<Type> InstancePtr; static InstancePtr p_; static boost::once_flag once_; }; template<typename Type> boost::once_flag Singleton<Type>::once_; template<typename Type> typename Singleton<Type>::InstancePtr Singleton<Type>::p_;
使用方法請參考源碼。
源碼:https://git.oschina.net/mkdym/DaemonSvc.git (主)&& https://github.com/mkdym/DaemonSvc.git (提升逼格用的)。
參考鏈接:
1. http://silviuardelean.ro/2012/06/05/few-singleton-approaches/ 請自備梯子
2. DCLP:http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
2015年10月25日星期日