Is your Singleton Broken?
Posted on May 5th, 2006 at 21:44 by fr3@K

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 instance2 (例中的 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 arithmetics4 之上的 singleton。

Singleton 的問題不只是一次性正確的建構與單一存取介面,還有相依性以及解構順序等等的問題。真要深入討論,可能夠我寫半本書了。也許以後會繼續寫多些。

近年來,我用的是 Header Driven Singleton,可以肯定不是最好的,但能符合正確運作的需求。問問你自己 - is your singleton broken?


[延伸閱讀]
COdE fr3@K » 拒絕 Singleton
COdE fr3@K » 啟始 Global Variable 的困境

  1. Scott Meyers 04 年的一份 slide (PDF) 簡單地說明了 GoF 的 double-checked locking 也是有問題的. Note: this footnote was added Oct 8th, 2008. []
  2. Class static instance 跟 global instance 的區別其實只有語法上的不同,語意則是沒任何差異的。前兩者與 local static instance - 也就是在 function 內部的 static instance,則還有啟始時機上的不同。 []
  3. 沒找到出處,自然這裡的名字也是我自己亂取的。 []
  4. Pthreads for WIN32 的 thread once 以及某些 Standard C++ Library 實做中的singleton就是架構在atomic arithmetics 之上。 []
del.icio.us:Is your Singleton Broken? digg:Is your Singleton Broken? spurl:Is your Singleton Broken? newsvine:Is your Singleton Broken? furl:Is your Singleton Broken? Y!:Is your Singleton Broken? 黑米共享書籤:Is your Singleton Broken? 推推王:Is your Singleton Broken?
Previous Post
« Epiphany Browser «
Next Post
» Version Control with Subversion »

8 Comments »

Pingback #78

[...] Is your Singleton Broken? – 解說各種 C++ singleton 的解法,各有優缺點。之前我曾經為了 Xerces-C2 與 Xalan-C 的初始化問題,被逼得也要搞一個 library 用的 singleton 機制[1],盡可能的僅利用語言機制解決,兩者之間的 dependency,以及多 DLL 混合的程式環境,使得問題更加的複雜。 [...]

Pingback by 今日連結 (2006-09-21) [JeffHung.Blog] — September 22, 2006 @ 9:26


Trackback #4063

拒絕 Singleton…

去年我寫過一篇文章, 大意是在說 大多數人所使用的 Singleton 實作都是有問題的. 在文章靠近後面的部份, 我也介紹了一種較少為人知, 我常用在 lifetime 是全局但 scope 是局部的 (global) 變數的 Si…

Trackback by COdE fr3@K — December 13, 2007 @ 13:33


Comment #4807

非常有意思,受教了。
其实作为project来说,大部分情况下GoF Singleton是可以适用的?
因为“static mutex mutex_;”在程序启动时已经完成,除非mutex_本身构建需要很长的时间

另外:
Codejock Software的Xtreme ToolkitPro v10.0中给出了另外一种实现方法,在foo类中,定义了foo类静态指针及一个私有类(垃圾收集器),并定义了该类的静态全局变量(static global variable),并在类的析构函数中析构foo的静态指针

Comment by neooranderson — February 3, 2009 @ 18:33


Comment #4808

Hi neooranderson,

Glad to be helpful.

其实作为project来说,大部分情况下GoF Singleton是可以适用的?
因为“static mutex mutex_;”在程序启动时已经完成,除非mutex_本身构建需要很长的时间

很遺憾, 除非不讓其他的 global instance (含 class/file static) 直接/間接參考到它, 否則用了 static mutex 就不行.

這與建構 mutex 所需的時間無關, 這是 dependency 與啟始順序的問題, 請參考我去年底寫的另一篇文字: 啟始 Global Variable 的困境

另外, 用在 GoF’s singleton 的 double-checked locking 也已經被證實是失敗的手法, 在多緒程序無法正確保障客戶端參考到的是起始完成的對象. 請參考 Meyers 的簡報.

Codejock Software的Xtreme ToolkitPro v10.0中给出了另外一种实现方法…

我沒玩過這東西不好評論它的優缺點, 但也不期望它會做的多好. Singleton 手法是為了解決 global variable 的技術問題, 但在我看來 global variable 更是一個設計問題. 用技術來 work around 糟糕的設計… 你說會是容易的事情嗎?

Comment by fr3@K — February 3, 2009 @ 19:59


Comment #5024

想請教一下,這個 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;;
}

Comment by koldkane — April 3, 2009 @ 17:48


Comment #5025

好問題. 但我要很不好意思地跟你說 – 我忘記了.

事實上我在後來的 啟始 Global Variable 的困境 中寫的 sample code 就是直接用 init() 對 global/singleton 初始化.

看到你的留言後我 revisit 了這段 code, 沒有看出 init_impl() 的必要性. 先讓我以 “它是沒有必要的” 來回答你. 如果之後我看出想出什麼端倪再留言並 email 給你.

Thanks for your comment.

Comment by fr3@K — April 3, 2009 @ 18:55


Comment #5641

屁的半死的blog…
了不起竟然再批評大師的作品,有種你寫一個來 Open Source Project!

    ed: 不雅無效 url, 已刪除 – fr3@K

Comment by fuck — April 25, 2009 @ 10:28


Comment #5682

哈哈哈, 謝謝你的提醒. 我的確是蠻屁的, 可是這篇文字沒有對文中提到的任何人或組織 (包括大師在內) 有不敬之意啊.

就像沒有任何現代物理人會否定牛頓的大師地位與貢獻, 但試問如果不正視牛頓力學的狹隘, 要如何才能欣賞相對論的美麗?

另, 本文雖是原創, 但我並不是第一個發表這樣看法的人, 我不過是一個對這些問題點稍有了解並願意將其公開文字化的人. 寫這篇文字的目的是對相關問題提供一篇整合性的中文討論. 當然, 不管是這篇或是站上的其他文字, 若有任何技術上不正確的地方, 還期待您與其他朋友的友善補充指正.

不要想太多了, 還是讓我們回到技術性主題的交流好嗎?

Comment by fr3@K — April 26, 2009 @ 16:27


Comments RSS TrackBack URI

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>