單例原本是個很簡單的模式,實現上應該也是很簡單,但C++單例的簡單實現會有一些坑,來看看爲了不這些坑怎樣一步步演化到boost庫的實現方式。安全
class QMManager { public: static QMManager &instance() { static QMManager instance_; return instance_; } }這是最簡單的版本,在單線程下(或者是C++0X下)是沒任何問題的,但在多線程下就不行了,由於static QMManager instance_;這句話不是線程安全的。
static QMManager &instance() { static bool constructed = false; static uninitialized QMManager instance_; if (!constructed) { constructed = true; new(&s) QMManager; //construct it } return instance_; }
這裏有競爭條件,兩個線程同時調用instance()時,一個線程運行到if語句進入後還沒設constructed值,此時切換到另外一線程,constructed值仍是false,一樣進入到if語句裏初始化變量,兩個線程都執行了這個單例類的初始化,就再也不是單例了。多線程
一個解決方法是加鎖:函數
static QMManager &instance() { Lock(); //鎖本身實現 static QMManager instance_; UnLock(); return instance_; }
但這樣每次調用instance()都要加鎖解鎖,代價略大。測試
那再改變一下,把內部靜態實例變成類的靜態成員,在外部初始化,也就是在include了文件,main函數執行前就初始化這個實例,就不會有線程重入問題了:spa
class QMManager { protected: static QMManager instance_; QMManager(); ~QMManager(){}; public: static QMManager *instance() { return &instance_; } void do_something(); }; QMManager QMManager::instance_; //外部初始化
這被稱爲餓漢模式,程序一加載就初始化,無論有沒有調用到。.net
看似沒問題,但仍是有坑,在一個2B狀況下會有問題:在這個單例類的構造函數裏調用另外一個單例類的方法可能會有問題。線程
看例子:code
//.h class QMManager { protected: static QMManager instance_; QMManager(); ~QMManager(){}; public: static QMManager *instance() { return &instance_; } }; class QMSqlite { protected: static QMSqlite instance_; QMSqlite(); ~QMSqlite(){}; public: static QMSqlite *instance() { return &instance_; } void do_something(); }; QMManager QMManager::instance_; QMSqlite QMSqlite::instance_;
//.cpp QMManager::QMManager() { printf("QMManager constructor\n"); QMSqlite::instance()->do_something(); } QMSqlite::QMSqlite() { printf("QMSqlite constructor\n"); } void QMSqlite::do_something() { printf("QMSqlite do_something\n"); }
這裏QMManager的構造函數調用了QMSqlite的instance函數,但此時QMSqlite::instance_可能尚未初始化。對象
這裏的執行流程:程序開始後,在執行main前,執行到QMManager QMManager::instance_;這句代碼,初始化QMManager裏的instance_靜態變量,調用到QMManager的構造函數,在構造函數裏調用QMSqlite::instance(),取QMSqlite裏的instance_靜態變量,但此時QMSqlite::instance_還沒初始化,問題就出現了。blog
那這裏會crash嗎,測試結果是不會,這應該跟編譯器有關,靜態數據區空間應該是先被分配了,在調用QMManager構造函數前,QMSqlite成員函數在內存裏已經存在了,只是還未調到它的構造函數,因此輸出是這樣:
QMManager constructor
QMSqlite do_something
QMSqlite constructor
那這個問題怎麼解決呢,單例對象做爲靜態局部變量有線程安全問題,做爲類靜態全局變量在一開始初始化,有以上2B問題,那結合下上述兩種方式,能夠解決這兩個問題。boost的實現方式是:單例對象做爲靜態局部變量,但增長一個輔助類讓單例對象能夠在一開始就初始化。以下:
//.h class QMManager { protected: struct object_creator { object_creator() { QMManager::instance(); } inline void do_nothing() const {} }; static object_creator create_object_; QMManager(); ~QMManager(){}; public: static QMManager *instance() { static QMManager instance; return &instance; } }; QMManager::object_creator QMManager::create_object_; class QMSqlite { protected: QMSqlite(); ~QMSqlite(){}; struct object_creator { object_creator() { QMSqlite::instance(); } inline void do_nothing() const {} }; static object_creator create_object_; public: static QMSqlite *instance() { static QMSqlite instance; return &instance; } void do_something(); }; QMManager::object_creator QMManager::create_object_; QMSqlite::object_creator QMSqlite::create_object_;
結合方案3的.cpp,這下能夠看到正確的輸出和調用了:
QMManager constructor
QMSqlite constructor
QMSqlite do_something
來看看這裏的執行流程:
初始化QMManager類全局靜態變量create_object_
->調用object_creator的構造函數
->調用QMManager::instance()方法初始化單例
->執行QMManager的構造函數
->調用QMSqlite::instance()
->初始化局部靜態變量QMSqlite instance
->執行QMSqlite的構造函數,而後返回這個單例。
跟方案三的區別在於QMManager調用QMSqlite單例時,方案3是取到全局靜態變量,此時這個變量未初始化,而方案四的單例是靜態局部變量,此時調用會初始化。
跟最初方案一的區別是在main函數前就初始化了單例,不會有線程安全問題。
上面爲了說明清楚點去除了模版,實際使用是用模版,不用寫那麼多重複代碼,這是boost庫的模板實現:
template <typename T> struct Singleton { struct object_creator { object_creator(){ Singleton<T>::instance(); } inline void do_nothing()const {} }; static object_creator create_object; public: typedef T object_type; static object_type& instance() { static object_type obj; //聽說這個do_nothing是確保create_object構造函數被調用 //這跟模板的編譯有關 create_object.do_nothing(); return obj; } }; template <typename T> typename Singleton<T>::object_creator Singleton<T>::create_object; class QMManager { protected: QMManager(); ~QMManager(){}; friend class Singleton<QMManager>; public: void do_something(){}; }; int main() { Singleton<QMManager>::instance()->do_something(); return 0; }
其實Boost庫這樣的實現像打了幾個補丁,用了一些奇技淫巧,雖然確實繞過了坑實現了需求,但感受挺很差的。
參考資料:
http://blog.csdn.net/fullsail/article/details/8483106
http://blog.cnbang.net/tech/2229/以上是爲實現單例在多線程運行中正常運行而作的修改。
下面介紹一下基於boost庫實現單例的簡單使用方法。
爲了讓單例使用起來更爲方便,可定義以下宏:
#define SINGLE(classname) Singleton<classname>::instance()
SINGLE(MyClass)->...
總結:
本文介紹了單例的實現方法,並介紹了簡單的單例封裝方法,下一篇將介紹單例的管理。