Singleton 是 C++ 程式設計裡,最古老的問題之一。典型的 (狹義的) singleton 是用一個 class 的 static member function 封裝,稱為 singleton pattern。The Gang of Four (GoF) 在 Design Patterns 討論並提出第一個 (在某種程度上似乎) 令人滿意的 singleton 解答,這個 pattern 幾乎是一夕之間聲名大噪,成為 (可能是) 最廣為人知的 pattern。不論在 GoF 之前與之後,許多人 (其中不乏大師級人物) 提出了不同的 singleton pattern,要解決的問題主要是:
- 確保某單一 instance 的存在
- 提供單一介面 (接口) 以存取該 instance
- 確保該 instance 能被正確建構與解構 (虛構)
- 確保該 instance 的有正確的生存週期
這裡的討論會把範圍放大,不隻 singleton pattern,還要含括廣義的 singleton,也就是 - global 變數 (以下除非特別註明,singleton 指的是廣義的 singleton)。
任何 non-trival 程式,幾乎都免不了使用到 singleton。也許你不知道,簡單如 C++ 的 hello world 也用到了 singleton:
-
#include <iostream> using namespace std; int main () { cout << "Hello, world" << endl; return 0; }
cout
就是一個 Standard C++ Library 定義的 singleton。
對使用者來說,global 變數與 singleton pattern 最明顯的差異是前者的介面就是該 instance 本身,而後者需要經過一個 wrapper function。但除此之外,對實做者來說,面對的問題依然是前面提到的幾點。
最簡單的 singleton pattern 大緻上是這樣子的:
-
class foo { private: static foo foo_instance_; foo (); ~foo (); foo& foo (const foo&); foo& operator= (const foo&); public: static foo& instance () { return foo_instance_; } };
本篇的 sample code 都是經過簡化的,很可能無法正確執行。由於資料不在手邊,即便已努力在網路上找相關 reference,所列舉的 singleton pattern,還是有不少是我記憶裡碎片的拼湊。如有錯誤,請別客氣幫忙指正。
Meyer’s Singleton
第一個廣為人知的 singleton pattern 之一,是由 Scott Meyers 提出 (常稱為 Meyer’s Singleton)。通常看起來像是這樣:
-
class foo { private: foo (); ~foo (); foo& foo (const foo&); foo& operator= (const foo&); public: static foo& instance () { static foo foo_instance; return foo_instance; } };
Meyer’s Singleton 不但簡潔易懂,還兼具了 lazy initialization (on first reference) 的 optimization。隻可惜這個優雅的解答在 multithreading 下無法確保 instance 在被 reference 到時已經被正確啟始完成 (前列”簡單”singleton 也有類似的問題)。
GoF Singleton
GoF 提出的 singleton pattern 本質上與 Meyer’s Singleton 類似,但加入了解決在 multithreading 下確保instance 在被 reference 到時能完成正確啟始的機制以及所謂 double-check 的 optimization,以在非必要情況下免除 threading synchronization 的 overhead:
-
class scoped_guard; class mutex; class foo { private: static mutex mutex_; static foo* foo_ptr_; foo (); ~foo (); foo& foo (const foo&); foo& operator= (const foo&); public: static foo& instance () { if(foo_ptr_ == 0) { scoped_guard g(mutex_); if(foo_ptr_ == 0) foo_ptr_ = new foo; } return *foo_ptr_; } };
忘記了 GoF 在這例子裡是不是故意把解構的部份留給讀者當作練習題目,還是我忘了它的解構機制。總之,用
atexit()
或其他方式加上解構機制不會是件太困難的事。
GoF’s singleton 有它本身的問題,其中我個人認為最搞笑的是 - 它用了個輔助性的 global instance1 (例中的 mutex)。已知 - global instance 在 multithreading 下有啟始上的問題,試問 - 如何用一個 global instance 去確保另一個 global instance 的啟始正確?這是個被 GoF 大師們忽略掉的問題 - 一個雞生蛋蛋生雞的沖突。
Loki’s Singleton
前面提到的兩個 singleton pattern,都是大師級人物在十幾年前所發表的。而在幾年前 (兩千年後?),鬼才 Andrei Alexandrescu 發表了在當時對許多人來說可說是嚇死人不償命的跨世紀巨作 - Modern C++ Design 以及 Loki Library 。他提出了不少革命性的思維,把 policy-based design 用到了幾近極緻的程度,以及前所未見的 Type List 等等。即使對一個不欣賞 Alexandrescu 風格的人如我,也從這本書中深刻的感受到新典範的 generic programming 威力。
Loki 把 singleton 予以泛化,更用不同 policy 控制 singleton 的生命週期,啟始時機,啟始順序, thread- safety 等等行為。但還是掉進了 GoF 當初陷入的陷阱 - 用 global instance 去確保另一個 global instance 的正確啟始。
POSIX Threads’ Singleton
POSIX Threads 標準定義了的 thread once 的 interface:
-
// in source file pthread_once_t once_control = PTHREAD_ONCE_INIT; foo* ptr_; void cleanup () { delete ptr_; } void init () { ptr_ = new foo; atexit(cleanup); } foo& instance () { pthread_once(&once_control, init); return *ptr_; }
在這個例子裡 thread once 能確保不管
instance()
在任何情況下被呼叫,
init()
隻會被”安全”地 (thread-safe) 呼叫一次,任何在
pthread_once()
後面的操作都可以放心假設
init()
已經完成。
Header Driven Singleton
這個 singleton pattern 出處我一直找不到也忘了是在那裡先看到的:2
-
// header file bool init (); foo& instance (); namespace { // The next line would be injected to every object file, those include this header file. const bool init__ = init(); } // source file foo& instance () { static foo instance_; return instance_; } bool init_impl () { instance() return true; } bool init () { static bool ret = init_impl(); return ret; }
這個 pattern 的原理是把每一個 object file 都 inject 一段啟始 singleton 的程式。優點如下:
- (大多數時候) 使用者不需要注意不同 (存在於數個 object file 中) singleton 的建構與解構順序
- “真正”能夠在 multithreading 下確保參考時,instance 已完成啟始
- 執行時期 overhead 小
- 不依賴除了 C++ 語言之外的功能
- 程式開始時 singleton 就已建構完成 (我一直不喜歡用在 singleton 的 lazy initialization)
缺點則是在間接使用到這種 singleton 時,include file 的 dependency 需要特別注意。可能會遇到編譯器不能幫你抓出的 dependency 問題。
Conclusion
還有許多 singleton pattern 沒在這裡提到,大多數是上述的 variant,多是加強了 nice to have 的功能,卻沒有解決基本上的問題。當然,也有本質上不同的,例如建立在 atomic arithmetics3 之上的 singleton。
Singleton 的問題不隻是一次性正確的建構與單一存取介面,還有相依性以及解構順序等等的問題。真要深入討論,可能夠我寫半本書了。也許以後會繼續寫多些。
近年來,我用的是 Header Driven Singleton,可以肯定不是最好的,但能符合正確運作的需求。問問你自己 - is your singleton broken?
[延伸閱讀] 拒絕 Singleton
- Class static instance 跟 global instance 的區別其實隻有語法上的不同,語意則是沒任何差異的。前兩者與 local static instance - 也就是在 function 內部的 static instance,則還有啟始時機上的不同。 [↩]
- 沒找到出處,自然這裡的名字也是我自己亂取的。 [↩]
- Pthreads for WIN32 的 thread once 以及某些 Standard C++ Library 實做中的singleton就是架構在atomic arithmetics 之上。 [↩]