Is your Singleton Broken?

来源:互联网 发布:sip服务器软件 编辑:程序博客网 时间:2024/05/22 14:50

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 filepthread_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 filebool init ();foo& instance ();namespace{  // The next line would be injected to every object file, those include this header file.  const bool init__ = init();}// source filefoo& 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

 

  1. Class static instance 跟 global instance 的區別其實只有語法上的不同,語意則是沒任何差異的。前兩者與 local static instance - 也就是在 function 內部的 static instance,則還有啟始時機上的不同。 []
  2. 沒找到出處,自然這裡的名字也是我自己亂取的。 []
  3. Pthreads for WIN32 的 thread once 以及某些 Standard C++ Library 實做中的singleton就是架構在atomic arithmetics 之上。 []
原创粉丝点击