方案一
代碼如下 |
複製代碼 |
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函數執行前就初始化這個執行個體,就不會有線程重入問題了:
代碼如下 |
複製代碼 |
class QMManager { protected: static QMManager instance_; QMManager(); ~QMManager(){}; public: static QMManager *instance() { return &instance_; } void do_something(); }; QMManager QMManager::instance_; //外部初始化
|
這被稱為餓漢模式,程式一載入就初始化,不管有沒有調用到。
看似沒問題,但還是有坑,在一個2B情況下會有問題:在這個單例類的建構函式裡調用另一個單例類的方法可能會有問題。
看例子:
代碼如下 |
複製代碼 |
//.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 constructorn"); QMSqlite::instance()->do_something(); } QMSqlite::QMSqlite() { printf("QMSqlite constructorn"); } void QMSqlite::do_something() { printf("QMSqlite do_somethingn"); } |
這裡QMManager的建構函式調用了QMSqlite的instance函數,但此時QMSqlite::instance_可能還沒有初始化。
這裡的執行流程:程式開始後,在執行main前,執行到QMManager QMManager::instance_;這句代碼,初始化QMManager裡的instance_靜態變數,調用到QMManager的建構函式,在建構函式裡調用QMSqlite::instance(),取QMSqlite裡的instance_靜態變數,但此時QMSqlite::instance_還沒初始化,問題就出現了。
那這裡會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
上面為了說明清楚點去除了模版,實際使用是用模版,不用寫那麼多重複代碼,這是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庫這樣的實現像打了幾個補丁,用了一些奇技淫巧,雖然確實繞過了坑實現了需求,但感覺挺不好的。