細說Singleton模式:創建、多線程與銷毀
GOF著作中對Singleton模式的描述為:保證一個class只有一個實體(Instance),并為它提供一個全局訪問點(global access point)。
從其描述來看,是非常簡單的,但實現該模式卻是復雜的。Singleton設計模式不存在一種所謂的“***”方案。需要根據當時的具體問題進行具體解決,下面將講述在不同環境下的解決方案。
Singleton的詳細解釋,請大家看GOF的著作《設計模式》一書。俺比較懶,是不想抄了。
1 Singleton創建
1.1 GOF Singleton
在GOF著作中對Singleton模式的實現方式如下:
- /*解一*/
- class Singleton
- {
- public:
- static Singleton *Instance(){ //1
- if( !m_pInstatnce) //2
- m_pInstance = new Singleton;//3
- return m_pInstance; //4
- }
- private:
- static Singleton *m_pInstatnce; //5
- private:
- Singleton(); //6
- Singleton(const Singleton&); //7
- Singleton& operator=(const Singleton&); //8
- ~Singleton(); //9
- }
- Singleton *Singleton:m_pInstatnce = NULL; //10
在上面的解決方案中,我們只在需要調用時,才產生一個Singleton的對象。這樣帶來的好處是,如果該對象產生帶來的結果很昂貴,但不經常用到時,是一種非常好的策略。但如果該Instance被頻繁調用,那么就有人覺得Instance中的判斷降低了效率(雖然只是一個判斷語句^_^),那么我們就把第5條語句該為
- static Singleton m_Instatnce;
如此一來,在Instatnce直接返回&m_Instance,而不用做任何判斷,效率也高了。(是不是呢?)
這樣修改后,我們將帶來災難性的后果:
1:首先有可能編譯器這關就沒法通過,說m_Instance該外部變量無法解決(visural C++6.0)
- error LNK2001: unresolved external symbol "private: static class Singleton Singleton::m_Instance" (?m_Instance@Singleton@@0V1@A)
2:如果編譯器這關通過了就沒問題了么?答案是否定的。
***是不管Instance是否用到,該靜態變量對象在編譯器編譯時就產生了,即資源消耗是不可避免的;
第二是無法確保編譯器一定先將m_Instance初始化。所以Instance的調用有可能傳回一個尚沒構造的Singleton對象。這也意味著你無法保證任何外部對象所使用的m_Instance是一個被正確初始化的對象。
1.2 Meyers Singleton
我們如何解決這個問題呢,實際上很簡單。一種非常優雅的做法由Scott Meyers***提出,故也稱為Meyers Singleton。它依賴編譯器的神奇技巧。即函數內的static對象只在該函數***次執行時才初始化(請注意不是static常量)。
- /*解二*/
- class Singleton
- {
- public:
- static Singleton *Instance(){ //1
- static Singleton sInstance; //2
- return &sInstance; //3
- }
- private:
- Singleton(); //4
- Singleton(const Singleton&); //5
- Singleton& operator=(const Singleton&); //6
- ~Singleton(); //7
- }
解二在Instance中定義了一個Static的Singleton對象,來解決Instance中初始化的問題,也很順利的解決了定義Static成員對象帶來的問題。
請注意,解二在VC6中不能編譯通過,將有以下的錯誤:
- error C2248: 'Singleton::~Singleton' : cannot access private member declared in class 'Singleton' e:\work\q\a.h(81) : see declaration of 'Singleton::~Singleton'
產生該問題的錯誤原因是什么呢(請仔細思考^_^)
原因在于在產生static Singleton對象后,編譯器會自動產生一個銷毀函數__DestroySingleton,然后調用atexit()注冊,在程序退出時執行__DestroySingleton。但由于Singleton的析構函數是private,所以會產生訪問錯誤。(應該在以后的編譯器中修改了該BUG)
1.3 Singleton改進
讓Instance傳回引用(reference)。如果傳回指針,調用端有可能講它delete調。
1.4 Singleton注意之點
在上面的解法中,請注意對構造函數和析構函數的處理,有何好處(請自己理解,俺懶病又犯了)。
2 多線程
在解一中,如果我們運行在多線程的環境中,該方案是***的么,將會有什么后果呢?
后果就是會造成內存泄漏,并且有可能前后獲取的Singleton對象不一樣(原因請自己思考,后面有解答)。
為了解決這個問題,將解一的Instance改為如下:
- Singleton& Singleton::Instance(){
- Lock(m_mutex); //含義為獲取互斥量 //1
- If( !m_pInstance ){ //2
- m_pInstance = new Singleton; //3
- }
- UnLock(m_mutex); //4
- return *m_pInstance; //5
- }
此種方法將解決解一運行在多線程環境下內存泄漏的問題,但帶來的結果是,當m_mutex被鎖定時,其它試圖鎖定m_mutex的線程都將必須等等。并且每次執行鎖操作其付出的代價極大,亦即是這種方案的解決辦法并不吸引人。
那么我們將上面的代碼改為如下方式:
- Singleton& Singleton::Instance(){
- If( !m_pInstance ){ //1
- Lock(m_mutex); //含義為獲取互斥量 //2
- m_pInstance = new Singleton; //3
- UnLock(m_mutex); //4
- }
- return *m_pInstance; //5
- }
這樣修改的結果沒有問題了么?NO!!!!該方案帶來的結果同解一,原因也一樣,都將造成內存泄漏。此時“雙檢測鎖定”模式就粉墨登場了。
由Doug Schmidt和Tim Harrison提出了“雙檢測鎖定”(Double-Checked Locking)模式來解決multithread singletons問題。
- Singleton& Singleton::Instance(){
- If( !m_pInstance ){ //1
- Lock(m_mutex); //含義為獲取互斥量 //2
- If(!m_pInstance) //3
- m_pInstance = new Singleton; //4
- UnLock(m_mutex); //5
- }
- return *m_pInstance; //6
- }
請看上面的第三句,這句話是不是具有化腐朽為神奇的力量啊 ^_^
上面的方案就***了么。回答還是NO!!!(各位看官是否已經郁悶了啊,這不是玩我啊?請耐心點,聽我細細到來^_^)
如果在RISC機器上編譯器有可能將上面的代碼優化,在鎖定m_mutex前執行第3句。這是完全有可能的,因為***句和第3句一樣,根據代碼優化原則是可以這樣處理的。這樣一來,我們引以為自豪的“雙檢測鎖定”居然沒有起作用( L)
怎么辦?解決唄。怎么解決?簡單,我們在m_pInstance前面加一個修飾符就可以了。什么修飾符呢?……
àvolatile(簡單吧)
那么我們完整的解法如下:
- /*解三*/
- class Singleton
- {
- public:
- static Singleton &Instance(){ //1
- if( !m_pInstatnce){ //2
- Lock(m_mutex) //3
- If( !m_pInstance ) //4
- m_pInstance = new Singleton;//5
- UnLock(m_mutex); //6
- }
- return *m_pInstance; //7
- }
- private:
- static volatitle Singleton *m_pInstatnce; //8
- private:
- Singleton(); //9
- Singleton(const Singleton&); //10
- Singleton& operator=(const Singleton&); //11
- ~Singleton(); //12
- }
- Singleton *Singleton:m_pInstatnce = NULL; //13
3 Singleton銷毀
在這里,我們就到了Singleton最簡單也最復雜的地方了。
為什么說它簡單?我們根本可以不理睬創建的對象m_pInstance的銷毀啊。因為雖然我們一直沒有將Singleton對象刪除,但不會造成內存泄漏。為什么這樣說呢?因為只有當你分配了累積行數據并丟失了對他的所有reference是,內存泄漏才發生。而對Singleton并不屬于上面的情況,沒有累積性的東東,而且直到結束我們還有它的引用。在現代操作系統中,當一個進程結束后,將自動將該進程所有內存空間完全釋放。(可以參考《effective C++》條款10,里面講述了內存泄漏)。
但有時泄漏還是存在的,那是什么呢?就是資源泄漏。比如說如果該Singleton對象管理的是網絡連接,OS互斥量,進程通信的handles等等。這時我們就必須考慮到Singleton的銷毀了。談到銷毀,那可是一個復雜的課題(兩天三夜也說不完^_^ 開玩笑的啦,大家輕松一下嘛)。
我們需要在恰當的地點,恰當的時機刪除Singleton對象,并且還要在恰當的時機創建或者重新創建Singleton對象。
在我們的“解二”中,在程序結束時會自動調用Singleton的析構函數,那么也將自動釋放所獲取的資源。在大多數情況下,它都能夠有效運作。那特殊情況是什么呢?
我們以KDL(keyboard,display,log)模型為例,其中K,D,L均使用Singleton模式。只要keyboard或者display出現異常,我們就必須調用log將其寫入日志中,否則log對象不應該創建。對后面一條,我們的Singleton創建時就可以滿足。
在前面我們已經說到,在產生一個對象時(非用new產生的對象),由編譯器自動調用了atexit(__DestroyObject)函數來實現該對象的析構操作。而C++對象析構是LIFO進行的,即先產生的對象后摧毀。
如果在一般情況下調用了log對象,然后開始銷毀對象。按照“后創建的先銷毀”原則:log對象將被銷毀,然后display對象開始銷毀。此時display在銷毀發現出現異常,于是調用log對象進行記錄。但事實上,log對象已經被銷毀,那么調用log對象將產生不可預期的后果,此問題我們稱為Dead Reference。所以前面的解決方案不能解決目前我們遇到的問題。
Andrei Alexandrescu提出了解決方案,稱為Phoenix Singleton(取自鳳凰涅磐典故)
- /*解四*/
- class Singleton
- {
- public:
- static Singleton &Instance(){
- if( !m_pInstatnce){
- Lock(m_mutex)
- If( !m_pInstance ){
- if(m_destroyed)
- OnDeadReference();
- else
- Create();
- }
- UnLock(m_mutex);
- }
- return *m_pInstance;
- }
- private:
- static volatitle Singleton *m_pInstatnce;
- static bool m_destroyed;
- private:
- Singleton();
- Singleton(const Singleton&);
- Singleton& operator=(const Singleton&);
- ~Singleton(){
- m_pInstance = 0;
- m_destroyed = true;
- }
- static void Create(){
- static Singleton sInstance;
- m_pInstanace = &sInstance;
- }
- static void OnDeadReference(){
- Create();
- new (m_pInstance) Singleton;
- atexit(KillPhoenixSingleton);
- m_destroyed = false;
- }
- void KillPhoenixSingleton(){
- m_pInstance->~Singleton();
- }
- }
- Singleton *Singleton:m_pInstatnce = NULL;
- bool m_destroyed =false;
請注意此處OnDeadReference()中所使用的new操作符的用法:是所謂的placement new操作,它并不分配內存,而是在某個地址上構造一個新對象。
這是解決Dead Reference方法之一。如果此時keyboard或者display對象也需要處理Dead Reference問題時,那么上面的OnDeadReference將被頻繁調用,效率將會很低。即該問題為:需要提供一種解決方案,用于處理對象的建立過程可以不按照“先創建會銷毀”的原則,而應該為其指定一個銷毀順序。
聰明的Andrei Alexandrescu提出了一個“帶壽命的Singleton”解決方案。該方案的思想是:利用atexit()的特性;在每次創建一個對象后,將該對象放入到一個鏈表中(該鏈表是按照銷毀順序排訓的),并同時調用atexit()注冊一個銷毀函數;該銷毀函數從鏈表中獲取最需要銷毀的對象進行銷毀。(懶病又犯了L。該模式的實現請各位看官自行實現,可以參考《C++設計新思維》一書,Andrei Alexandrescu著)。
各位看官,看完本篇后,是否覺得Singleton還簡單啦 :)
【編輯推薦】