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,還是有不少是我記憶裡碎片的拼湊。如有錯誤,請別客氣幫忙指正。
Meyers' 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-checked locking 的 optimization, [1] 以在非必要情況下免除 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 instance [2] (例中的 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 出處我一直找不到也忘了是在那裡先看到的: [3]
// 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 const 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 arithmetics [4] 之上的 singleton。
Singleton 的問題不只是一次性正確的建構與單一存取介面,還有相依性以及解構順序等等的問題。真要深入討論,可能夠我寫半本書了。也許以後會繼續寫多些。
近年來,我用的是 Header Driven Singleton,可以肯定不是最好的,但能符合正確運作的需求。問問你自己 - is your singleton broken?
[延伸閱讀]
COdE fr3@K » 拒絕 Singleton
COdE fr3@K » 啟始 Global Variable 的困境
- Scott Meyers 04 年的一份 slide (PDF) 簡單地說明了 GoF 的 double-checked locking 也是有問題的. Note: this footnote was added Oct 8th, 2008. [↩]
- Class static instance 跟 global instance 的區別其實只有語法上的不同,語意則是沒任何差異的。前兩者與 local static instance - 也就是在 function 內部的 static instance,則還有啟始時機上的不同。 [↩]
- 沒找到出處,自然這裡的名字也是我自己亂取的。 [↩]
- Pthreads for WIN32 的 thread once 以及某些 Standard C++ Library 實做中的singleton就是架構在atomic arithmetics 之上。 [↩]

非常有意思,受教了。
其实作为project来说,大部分情况下GoF Singleton是可以适用的?
因为“static mutex mutex_;”在程序启动时已经完成,除非mutex_本身构建需要很长的时间
另外:
Codejock Software的Xtreme ToolkitPro v10.0中给出了另外一种实现方法,在foo类中,定义了foo类静态指针及一个私有类(垃圾收集器),并定义了该类的静态全局变量(static global variable),并在类的析构函数中析构foo的静态指针
Hi neooranderson,
Glad to be helpful.
很遺憾, 除非不讓其他的 global instance (含 class/file static) 直接/間接參考到它, 否則用了 static mutex 就不行.
這與建構 mutex 所需的時間無關, 這是 dependency 與啟始順序的問題, 請參考我去年底寫的另一篇文字: 啟始 Global Variable 的困境
另外, 用在 GoF’s singleton 的 double-checked locking 也已經被證實是失敗的手法, 在多緒程序無法正確保障客戶端參考到的是起始完成的對象. 請參考 Meyers 的簡報.
我沒玩過這東西不好評論它的優缺點, 但也不期望它會做的多好. Singleton 手法是為了解決 global variable 的技術問題, 但在我看來 global variable 更是一個設計問題. 用技術來 work around 糟糕的設計… 你說會是容易的事情嗎?
想請教一下,這個 header based singleton,為什麼要透過一個 init_impl(),而不是直接在 init() 初始化 singleton 就好了呢?
像這樣:
// 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 ()
{
instance()
return true;;
}
好問題. 但我要很不好意思地跟你說 – 我忘記了.
事實上我在後來的 啟始 Global Variable 的困境 中寫的 sample code 就是直接用 init() 對 global/singleton 初始化.
看到你的留言後我 revisit 了這段 code, 沒有看出 init_impl() 的必要性. 先讓我以 “它是沒有必要的” 來回答你. 如果之後我看出想出什麼端倪再留言並 email 給你.
Thanks for your comment.
屁的半死的blog…
了不起竟然再批評大師的作品,有種你寫一個來 Open Source Project!
ed: 不雅無效 url, 已刪除 – fr3@K
哈哈哈, 謝謝你的提醒. 我的確是蠻屁的, 可是這篇文字沒有對文中提到的任何人或組織 (包括大師在內) 有不敬之意啊.
就像沒有任何現代物理人會否定牛頓的大師地位與貢獻, 但試問如果不正視牛頓力學的狹隘, 要如何才能欣賞相對論的美麗?
另, 本文雖是原創, 但我並不是第一個發表這樣看法的人, 我不過是一個對這些問題點稍有了解並願意將其公開文字化的人. 寫這篇文字的目的是對相關問題提供一篇整合性的中文討論. 當然, 不管是這篇或是站上的其他文字, 若有任何技術上不正確的地方, 還期待您與其他朋友的友善補充指正.
不要想太多了, 還是讓我們回到技術性主題的交流好嗎?
路過請教一下,在 GoF 的設計裡面, mutex 雖然是 global 但是應該是 compile time 就可以決定初始值的吧? 而 instance() 是 runtime 才會用到,這樣這兩個應該是不衝突啊? 為什麼會有先後順序的問題造成沒有辦法 thread-safe 呢?
@xacid: 兩者都是 runtime, 只有 built-in types 與 POD types 才可以在 compile-time initialize.