Singleton模式是一種非常簡(jiǎn)單的設(shè)計(jì)模式,這種模式很常用也很容易被濫用。當(dāng)你設(shè)計(jì)應(yīng)用程序的時(shí)候,經(jīng)常會(huì)遇到某些對(duì)象在整個(gè)程序的生命周期應(yīng)該僅有一個(gè)實(shí)例的情況,比如File System,Graphic System,Logging Utility,這時(shí)候就可以用到Singleton模式。Singleton模式在GOF中描述如下:
Ensure a class only has one instance, and provide a global point of access to it.
Singleton模式的定義很簡(jiǎn)單,實(shí)現(xiàn)也有N多種,但是卻很難找到一個(gè)稱(chēng)得上“完美”的。實(shí)現(xiàn)一個(gè)完美的Singleton比想象中要難的多,下面探索性的來(lái)實(shí)現(xiàn)一個(gè)非完美的。
1.典型實(shí)現(xiàn)
在C++中,Singleton模式的典型實(shí)現(xiàn)如下:
1 // Header file Singleton.h
2 class Singleton {
3 public :
4 static Singleton& Instance() { // Unique point of access
5 if (0 == _instance)
6 _instance = new Singleton();
7 return * _instance;
8 }
9 void DoSomething();
10 private :
11 Singleton(); // Prevent clients from creating a new Singleton
12 ~Singleton(); // Prevent clients from deleting a Singleton
13 Singleton(const Singleton&); // Prevent clients from copying a Singleton
14 Singleton& operator=(const Singleton& );
15 private :
16 static Singleton *_instance; // The one and only instance
17 };
18
19 // Implementation file Singleton.cpp
20 Singleton* Singleton::_instance = 0;
通過(guò)將Singleton的構(gòu)造函數(shù)設(shè)為private可以禁止客戶(hù)代碼直接創(chuàng)建Singleton對(duì)象,除此之外,Singleton的copy constructor和copy assignment operator都為private且僅有聲明沒(méi)有實(shí)現(xiàn),禁止了客戶(hù)代碼拷貝Singleton對(duì)象。唯一可以創(chuàng)建Singleton對(duì)象的是Singleton自己的靜態(tài)成員函數(shù)Instance,這樣就在編譯器保證了Singleton實(shí)例的唯一性。上面這些是在C++中實(shí)現(xiàn)Singleton模式最基本的要點(diǎn)。
Instance方法保證只有在第一次調(diào)用時(shí)才會(huì)生成Singleton對(duì)象,以后的調(diào)用只是簡(jiǎn)單返回唯一的已存在的實(shí)例。Instance方法實(shí)際上實(shí)現(xiàn)的是懶惰初始化(lazy initialize),如果程序中根本沒(méi)有用到Singleton對(duì)象,也就根本不會(huì)產(chǎn)生Singleton的實(shí)例,這在Singleton對(duì)象很少使用且創(chuàng)建Singleton對(duì)象開(kāi)銷(xiāo)比較大的情況下特別有用。
客戶(hù)代碼現(xiàn)在可以這樣使用Singleton:
1 Singleton &s = Singleton::Instance();
2 s.DoSomething();
還需要說(shuō)明的是Singleton的析構(gòu)函數(shù),析構(gòu)函數(shù)也為private可以禁止客戶(hù)寫(xiě)出如下代碼。如果某個(gè)客戶(hù)寫(xiě)出了如下代碼,隨后的對(duì)Singleton的訪問(wèn)就會(huì)導(dǎo)致為定義行為,因?yàn)镾ingleton對(duì)象已經(jīng)不存在。
1 Singleton *p = & Singleton::Instance();
2 delete p;
2.引入smart pointer
上面的實(shí)現(xiàn)算是一個(gè)好的實(shí)現(xiàn)嗎?當(dāng)然不是,或許連一個(gè)正確的實(shí)現(xiàn)都算不上。如果你想湊合,當(dāng)然沒(méi)問(wèn)題,上面的代碼大多數(shù)情況下可以工作的很好。也許你已經(jīng)注意到了一些問(wèn)題,比如說(shuō)在上面的代碼中只有new沒(méi)有delete。是的,你說(shuō)會(huì)發(fā)生memory leak對(duì)吧,其實(shí)memory leak都不是主要的問(wèn)題,所有的現(xiàn)代操作系統(tǒng)在進(jìn)程結(jié)束的時(shí)候都會(huì)對(duì)內(nèi)存很好的進(jìn)行回收。比memory leak更值得讓人擔(dān)憂的是resource leak,如果Singleton在構(gòu)造函數(shù)中請(qǐng)求了某些資源:網(wǎng)絡(luò)連接,文件句柄,數(shù)據(jù)庫(kù)連接等。這些資源將得不到釋放。
唯一修正resource leak的方法就是在程序結(jié)束的時(shí)候delete _instance。當(dāng)然了,用smart pointer再好不過(guò),在這里用auto_ptr就可以滿足需要了(如果你還不知道smart_ptr是什么,花點(diǎn)時(shí)間熟悉C++標(biāo)準(zhǔn)庫(kù)吧),修改后的代碼如下:
1 // Header file Singleton.h
2 class Singleton {
3 public :
4 static Singleton& Instance() { // Unique point of access
5 if (0 == _instance.get())
6 _instance.reset(new Singleton());
7 return * (_instance.get());
8 }
9 void DoSomething(){}
10 private :
11 Singleton(){} // Prevent clients from creating a new Singleton
12 ~Singleton(){} // Prevent clients from deleting a Singleton
13 Singleton(const Singleton&); // Prevent clients from copying a Singleton
14 Singleton& operator=(const Singleton& );
15 private :
16 friend auto_ptr<Singleton> ;
17 static auto_ptr<Singleton> _instance; // The one and only instance
18 };
19
20 // Implementation file Singleton.cpp
21 auto_ptr<Singleton> Singleton::_instance;
3.用atexit替換smart pointer
C++并沒(méi)有規(guī)定不同編譯單元(translation unit,簡(jiǎn)單說(shuō)就是一個(gè)可編譯的cpp文件)中static對(duì)象的初始化順序。如果一個(gè)程序中有多個(gè)Singleton對(duì)象,那么這些Singleton對(duì)象的析構(gòu)順序也將是任意的。很顯然,當(dāng)多個(gè)Singleton對(duì)象有依賴(lài)關(guān)系時(shí),smart pointer根本無(wú)法保證Singleton的析構(gòu)順序。
msdn中對(duì)atexit描述如下:
The atexit function is passed the address of a function (func) to be called when the program terminates normally. Successive calls to atexit create a register of functions that are executed in last-in, first-out (LIFO) order. The functions passed to atexit cannot take parameters. atexit use the heap to hold the register of functions. Thus, the number of functions that can be registered is limited only by heap memory.
需要說(shuō)明的是atexit并不比smart pointer好多少,LIFO的保證對(duì)于有復(fù)雜依賴(lài)關(guān)系的多個(gè)Singleton依然束手無(wú)力,但是用atexit替換smart pointer卻是必須的,它是設(shè)計(jì)完美Singleton的基礎(chǔ)。
#如果你疑惑atexit為什么還是不行,請(qǐng)考慮下面的情況:
NOTE:下面的情況在Modern C++ Design中叫做KDL(Keyboard,Display,Log)problem。
某個(gè)程序中使用了如下3個(gè)Singleton:Keyboard,Display,Log。Keyboard和Display分別對(duì)應(yīng)于計(jì)算機(jī)的鍵盤(pán)和顯示器,Log用來(lái)記錄錯(cuò)誤信息。假設(shè)當(dāng)Keyboard和Display的構(gòu)造函數(shù)和析構(gòu)函數(shù)出現(xiàn)錯(cuò)誤時(shí)會(huì)調(diào)用Log記錄錯(cuò)誤信息,并且構(gòu)造和析構(gòu)導(dǎo)致的任何錯(cuò)誤都會(huì)終止程序。
在程序啟動(dòng)時(shí),如果Keyboard構(gòu)造成功,Display構(gòu)造失敗,很顯然在Display的構(gòu)造函數(shù)中將會(huì)構(gòu)造Log而且失敗信息會(huì)被Log記錄,根據(jù)假設(shè)這時(shí)候程序準(zhǔn)備退出,atexit注冊(cè)的函數(shù)將會(huì)按LIFO的順序被調(diào)用。因?yàn)?span style="COLOR: #333399">Keyboard先于Log構(gòu)造,所以Log先于Keyboard析構(gòu),但是當(dāng)由于某種原因Keyboard在析構(gòu)時(shí)失敗,想要調(diào)用Log記錄錯(cuò)誤信息時(shí),Log早已被銷(xiāo)毀,則Log::Instance()將會(huì)導(dǎo)致未定義行為。
#atexit的嚴(yán)重問(wèn)題:
從上面的例子可以看出,atexit和smart pointer相比僅僅是有LIFO的保證而已,這樣的保證貌似也不怎么有效,因?yàn)閍texit跟smart pointer一樣也無(wú)法解決KDL probleam。
atexit由于LIFO帶來(lái)了另外的問(wèn)題,看下面的代碼:
1 #include <cstdlib>
2 void Bar() {
3 ...
4 }
5 void Foo() {
6 std::atexit(Bar);
7 }
8 int main() {
9 std::atexit(Foo);
10 return 0 ;
11 }
上面的小段代碼用atexit注冊(cè)了Foo,F(xiàn)oo調(diào)用了std::atexit(Bar)。當(dāng)程序退出時(shí),根據(jù)atexit的LIFO保證,Bar在Foo之后注冊(cè),因此Bar應(yīng)該在Foo之前調(diào)用,但是當(dāng)Bar注冊(cè)的時(shí)候Foo已經(jīng)調(diào)用了,Bar根本就沒(méi)有機(jī)會(huì)能夠在Foo之前調(diào)用。這明顯自相矛盾對(duì)吧,沒(méi)辦法,C++標(biāo)準(zhǔn)好像忽視了這一點(diǎn),因此如果類(lèi)似代碼被調(diào)用,肯定不會(huì)有什么好的結(jié)果,好一點(diǎn)是resource leak,差一點(diǎn)估計(jì)程序就崩潰了!!!
atexit的這個(gè)問(wèn)題跟Singleton有關(guān)系嗎?當(dāng)然有,如果在一個(gè)Singleton的析構(gòu)函數(shù)中調(diào)用atexit就會(huì)出現(xiàn)上述問(wèn)題。即在KDL problem中,如果Keyboard和Display都構(gòu)造成功,當(dāng)Keyboard或Display任意一個(gè)析構(gòu)失敗時(shí),Keyboard或Display在析構(gòu)函數(shù)中會(huì)構(gòu)造Log,Log的構(gòu)造函數(shù)會(huì)間接調(diào)用atexit。oops!!!,可怕的未定義行為。
看到這里你一定對(duì)atexit相當(dāng)失望,貌似它帶來(lái)的好處多于壞處。但是請(qǐng)你相信,如果適當(dāng)設(shè)計(jì),atexit在后面的Singleton改造中會(huì)起到很重要的作用。
用atexit后的代碼:
1 // Header file Singleton.h
2 class Singleton {
3 public :
4 static Singleton& Instance() { // Unique point of access
5 if (0 == _instance) {
6 _instance = new Singleton();
7 atexit(Destroy); // Register Destroy function
8 }
9 return * _instance;
10 }
11 void DoSomething(){}
12 private :
13 static void Destroy() { // Destroy the only instance
14 if ( _instance != 0 ) {
15 delete _instance;
16 _instance = 0 ;
17 }
18 }
19 Singleton(){} // Prevent clients from creating a new Singleton
20 ~Singleton(){} // Prevent clients from deleting a Singleton
21 Singleton(const Singleton&); // Prevent clients from copying a Singleton
22 Singleton& operator=(const Singleton& );
23 private :
24 static Singleton *_instance; // The one and only instance
25 };
26
27 // Implementation file Singleton.cpp
28 Singleton* Singleton::_instance = 0;
你有沒(méi)有仔細(xì)考慮過(guò)Destroy中的_instance = 0;這一行代碼,上述代碼實(shí)際上實(shí)現(xiàn)的是
不死鳥(niǎo)模式(The Phoenix Singleton),所謂不死鳥(niǎo),就跟一輝一樣可以死而復(fù)生。上面的代碼可以解決本文最早提出的
KDL problem,即如果
Keyboard析構(gòu)失敗,雖然
Log已經(jīng)析構(gòu),但是由于Destroy中的_instance = 0;這一行代碼,Log::Instance()將會(huì)創(chuàng)建一個(gè)新的
Log對(duì)象,程序?qū)?huì)表現(xiàn)良好。當(dāng)然了,
Phoenix Singleton僅能用于無(wú)狀態(tài)的Singleton,如果
Log需要保存某些狀態(tài),
Phoenix Singleton也不會(huì)帶來(lái)任何好處。你當(dāng)然可以用某些方法維持
Phoenix Singleton的狀態(tài),但是在做之前先想想看是否值得,維持狀態(tài)可能會(huì)使Singleton變得特別復(fù)雜。
上面的
Phoenix Singleton已經(jīng)可以滿足大部分需要,如果你的Singleton沒(méi)有涉及到多線程,多個(gè)Singleton之間也沒(méi)有依賴(lài)關(guān)系,你大可以放心使用。但是如果你用到多線程,或者你的Singleton關(guān)系如
KDL般復(fù)雜,或者你覺(jué)得對(duì)每一個(gè)Singleton都敲同樣的代碼讓你厭煩。在后面幾篇會(huì)有一個(gè)
多線程安全的,能夠
解決多個(gè)Singleton依賴(lài)關(guān)系的,
基于模板的Singleton實(shí)現(xiàn)。