本篇介紹 c++ design pattern 的 singleton 單例模式,singleton 常被用來解決問題,許多時候整個系統只需要有一個的全域類別,這樣方便協調系統整體的行為。
一個類別在整個程式中只有一個實例,並且提供全域的存取,這就是單例模式 singleton pattern。
本文的目錄如下,
什麼是單例?
為什麼要用單例?
餓漢模式 Eager Singleton
懶漢模式 Lazy Singleton
雙重鎖 Double-Checked Locking
Meyers Singleton (最簡單好用的懶漢模式,也是常見的實作方式)
如何使用單例?
誰也使用了單例模式 Singleton Pattern ?
那就開始本文吧!
什麼是單例? 單例是一種常用的軟體設計模式 design pattern,使用單例這個類別必須保證只能有一個實例 instance 存在。
為什麼要用單例? 有些物件只需要一個實例 instance,某段程式碼只能初始化一次,例如伺服器程式中通常會把設定檔資訊讀入到一個單例物件,之後使用單例 getInstance 然後就可以在任何地方(其他類別)獲得這些設定資訊,其他的類似應用情形還有使用者登入、資料庫連線、快取、與驅動程式溝通、執行緒池等等。
市面上實作 c++ singleton 有好幾種,這邊只提比較常用以及通用的方式,這邊就要開始講古了, Singleton 分為兩種類型,分別為
餓漢模式 Eager Singleton
懶漢模式 Lazy Singleton
餓漢模式 Eager Singleton 這邊介紹的是餓漢模式,這版本會在進入 main 之前就將 Singleton 初始化好,原因是因為 static member variable 會被視為全域變數,
cpp-singleton-hungry.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <iostream> #include <thread> using namespace std ;class Singleton {public : static Singleton& getInstance () { cout << "Singleton getInstance\n" ; this_thread::sleep_for(chrono::seconds(1 )); return sInstance; } private : Singleton() { cout << "Singleton constructor\n" ; } static Singleton sInstance; }; Singleton Singleton::sInstance; int main () { cout << "main" << endl ; thread t1 ([]{ cout << "singleton addr: " << &Singleton::getInstance() << endl ; }) ; cout << "singleton addr: " << &Singleton::getInstance() << endl ; t1.join(); return 0 ; }
輸出如下,這邊故意在 getInstance()
裡 sleep 1 秒來測試多執行緒環境下也是沒問題的,1 2 3 4 5 6 Singleton constructor main Singleton getInstance Singleton getInstance singleton addr: 0x6052a9 singleton addr: 0x6052a9
缺點是如果該 Singleton 初始化很耗時的話會佔用到進入 main 前的時間, 優點是在進入 main 之前就將 Singleton 初始化好,所以不會有 thread-safe 執行緒安全問題,
需要注意的是在多個 Singleton 有相依關係的話會產生問題,例如 SingletonA 和 SingletonB 都採用了餓漢模式,SingletonA 初始化時需要 SingletonB,而這兩個 instance 又在不同的編譯單元(cpp檔),那麼這兩個的初始化是不固定順序的,如果 SingletonA 在 SingletonB 之前初始化就會出錯。解決方式避開多個 Singleton 有相依關係的設計,或者採用懶漢模式。
懶漢模式 Lazy Singleton 以下篇幅都要來介紹懶漢模式,懶漢模式意思為要用到時才初始化,
範例如下,這邊 getInstance()
初始化完 instance 後改成回傳 reference 而不是 pointer,因為你總不希望別人不小心把你的 instance 給 free 了吧!而別人是無法 free 一個 reference 的,所以回傳 refernce 是比較好的作法。cpp-singleton-lazy.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Singleton {public : static Singleton& getInstance () { if (sInstance == nullptr ) { sInstance = new Singleton(); } return *sInstance; } private : Singleton() {} static Singleton* sInstance; }; Singleton* Singleton::sInstance = nullptr ;
跟餓漢模式相比這種方法的好處是直到 getInstance()
被呼叫才會初始化,也稱為延遲初始化 (Lazy Initialization),這在一些初始化時消耗較大的情況有很大優勢。
而這個版本不是 thread-safe 的,如下列範例所示,假如現在有主執行緒和執行緒t1兩個執行緒都通過了 sInstance == nullptr
的判斷,那麼主執行緒和 t1 都會 new Singleton()
,那麼就不是單例了。
cpp-singleton-lazy2.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <iostream> #include <thread> using namespace std ;class Singleton {public : static Singleton& getInstance () { cout << "Singleton getInstance\n" ; if (sInstance == nullptr ) { this_thread::sleep_for(chrono::seconds(1 )); sInstance = new Singleton(); } return *sInstance; } private : Singleton() { cout << "Singleton constructor\n" ; } static Singleton* sInstance; }; Singleton* Singleton::sInstance = nullptr ; int main () { cout << "main" << endl ; thread t1 ([]{ cout << "singleton addr: " << &Singleton::getInstance() << endl ; }) ; cout << "singleton addr: " << &Singleton::getInstance() << endl ; t1.join(); return 0 ; }
輸出如下,可以看出建構子初始化了兩次,印出來的記憶體位置也不一樣,1 2 3 4 5 6 7 main Singleton getInstance Singleton getInstance Singleton constructor singleton addr: 0x138bda0 Singleton constructor singleton addr: 0x7f3a040008c0
單例模式在多執行緒環境下需要特別小心。若多個執行緒同時呼叫getInstance()
,可能會導致多個實例的建立。這時候可以使用雙重檢查鎖定(Double-Checked Locking)或是標準函式庫中的std::call_once
來解決這個問題。
雙重檢測上鎖版 Double-Checked Locking 基於前一個懶漢模式例子,在多執行緒的情況下會有多次初始化實例的情形,所以很簡單的修改方式為加個鎖,就產生下面這樣的簡單上鎖版,
cpp-singleton-lock.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <iostream> #include <thread> #include <mutex> using namespace std ;class Singleton {public : static Singleton& getInstance () { cout << "Singleton getInstance\n" ; lock_guard<std ::mutex> lock(sMutex); if (sInstance == nullptr ) { this_thread::sleep_for(chrono::seconds(1 )); sInstance = new Singleton(); } return *sInstance; } private : Singleton() { cout << "Singleton constructor\n" ; } static mutex sMutex; static Singleton* sInstance; }; mutex Singleton::sMutex; Singleton* Singleton::sInstance = nullptr ; int main () { cout << "main" << endl ; thread t1 ([]{ cout << "singleton addr: " << &Singleton::getInstance() << endl ; }) ; cout << "singleton addr: " << &Singleton::getInstance() << endl ; t1.join(); return 0 ; }
輸出如下,即使在初始化前 sleep 1 秒也能順利的產生一個實體。1 2 3 4 5 6 main singleton addr: Singleton getInstance singleton addr: Singleton getInstance Singleton constructor 0x7ff47bc026f0 0x7ff47bc026f0
但是這樣的寫法在多執行緒的情況下很頻繁地呼叫 getInstance 會造成 race condition 問題,產生效能低落的情形,因此有了雙重檢測上鎖版 Double-Checked Locking 這個版本的改良。
由 Meyers 的 C++ and the Perils of Double-Checked Locking 這篇 paper 提出 Double-Checked Locking 版本,
cpp-singleton-lock2.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <iostream> #include <thread> #include <mutex> using namespace std ;class Singleton {public : static Singleton& getInstance () { cout << "Singleton getInstance\n" ; if (sInstance == nullptr ) { lock_guard<std ::mutex> lock(sMutex); if (sInstance == nullptr ) { sInstance = new Singleton(); } } return *sInstance; } private : Singleton() { cout << "Singleton constructor\n" ; } static mutex sMutex; static Singleton* sInstance; }; mutex Singleton::sMutex; Singleton* Singleton::sInstance = nullptr ; int main () { cout << "main" << endl ; thread t1 ([]{ cout << "singleton addr: " << &Singleton::getInstance() << endl ; }) ; cout << "singleton addr: " << &Singleton::getInstance() << endl ; t1.join(); return 0 ; }
這邊會產生一個問題是當有一個執行緒在執行 sInstance = new Singleton();
,有另外一個執行緒在檢查第一個 sInstance == nullptr
時很有可能出現問題,
因為 sInstance = new Singleton();
這語句會分成三個步驟, Step 1: Allocate memory to hold a Singleton object. 記憶體配置 Step 2: Construct a Singleton object in the allocated memory. 在己經配置的記憶體上建構 Singleton 物件 Step 3: Make sInstance point to the allocated memory. 將 sInstance 指向配置的記憶體
1 2 3 4 5 6 7 8 9 10 11 static Singleton& getInstance () { if (sInstance == nullptr ) { lock_guard<std ::mutex> lock(sMutex); if (sInstance == nullptr ) { sInstance = operator new (sizeof (Singleton)); new (sInstance) Singleton; } } return *sInstance; }
這順序很可能會變成 1->3->2,導致在1->3時 sInstance 已經不是 null,另外一個執行緒執行 sInstance == nullptr
時判斷 sInstance 已經有指向到有效的物件,就直接拿來使用,結果就會發生災難~
更多詳細內容與解決方式請看 Meyers 的 C++ and the Perils of Double-Checked Locking 這篇 paper 內容,這邊就不多做介紹。
有鑒於上面的各種坑,個人比較建議接下來介紹的這種方式。
Meyers Singleton (最簡單好用的懶漢模式,也是常見的實作方式) 在 Scott Meyers 大神的《Effective C++ 》書中條款 4 提出的 local static object 實作方式,也是屬於懶漢模式,利用 local static object 在函式第一次被呼叫使用才初始化的特性,這個作法在 C++11 以後是保證 thread-safe 的,Visual Studio 從 Visual Studio 2015 開始支援,GCC 從 GCC 4.3 開始支援 引用 C++11 standard 的 §6.7.4:
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
以下為 Meyers Singleton 的實作方式,cpp-singleton-meyers.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <iostream> #include <thread> using namespace std ;class Singleton {public : static Singleton& getInstance () { static Singleton sInstance; return sInstance; } private : Singleton() {} }; int main () { cout << "main" << endl ; thread t1 ([]{ cout << "singleton addr: " << &Singleton::getInstance() << endl ; }) ; cout << "singleton addr: " << &Singleton::getInstance() << endl ; t1.join(); return 0 ; }
很簡單吧!
如何使用單例? 剛剛前幾節著重在介紹 C++ 的 singleton 幾種作法,這邊要介紹如何使用單例以及實際上在使用時應該需要注意的部份, 在這篇 stackoverflow 討論到 constructor 必須是 private 的,避免被別人呼叫建構實例化,同樣地,copy constructor 複製建構子與 assignment operator 賦值運算子 (operator=) 也是比照辦理,
假設我有一個 Setting class 提供全域的存取,以下為 c++ singleton 單例模式的範例(Meyers Singleton 版本),cpp-singleton.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <iostream> using namespace std ;class Setting {public : static Setting& getInstance () { static Setting instance; return instance; } Setting(Setting const &) = delete ; void operator =(Setting const &) = delete ; private : Setting() {} }; int main () { Setting &s = Setting::getInstance(); Setting &s2 = Setting::getInstance(); cout << "setting addr: " << &s << endl ; cout << "setting2 addr: " << &s2 << endl ; return 0 ; }
輸出如下,1 2 3 $ g++ singleton.cpp -std=c++11 && ./a.out setting addr: 0x10e6ff110 setting2 addr: 0x10e6ff110
誰也使用了單例模式 Singleton Pattern ? 誰也使用了 Singleton Pattern 單例模式? 我們來看看別人是怎麼使用的吧! 這邊舉個 OpenCV 內部的 VideoBackendRegistry class 為例, 使用 VideoBackendRegistry::getInstance()
來取得實例,所以在整個程式裡 VideoBackendRegistry 這個 class 就只會有一份實例,https://github.com/opencv/opencv/blob/master/modules/videoio/src/videoio_registry.cpp modules/videoio/src/videoio_registry.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class VideoBackendRegistry { protected : VideoBackendRegistry() { } public : static VideoBackendRegistry& getInstance () { static VideoBackendRegistry g_instance; return g_instance; } }; const std ::vector <VideoBackendInfo> result = VideoBackendRegistry::getInstance().getAvailableBackends_CaptureByIndex();
在 OpenCV highgui 模組裡因應不同平台下有不同的實作方式, 在 Windows 平台下 window_w32.cpp 裡的 getInstance()
也是使用 static local 的方式,只是回傳的是 shared_ptrhttps://github.com/opencv/opencv/blob/master/modules/highgui/src/window_w32.cpp modules/highgui/src/window_w32.cpp 1 2 3 4 5 static std ::shared_ptr <Win32BackendUI>& getInstance(){ static std ::shared_ptr <Win32BackendUI> g_instance = std ::make_shared<Win32BackendUI>(); return g_instance; }
AOSP (Android Open Source Project) 裡面 libutils 提供的 Singleton.h class 採用的是單一鎖方式,明顯地這種方式在多執行緒被頻繁呼叫的話會有 race condition 問題,2017 年在程式碼裡改成禁用,並建議改用 scoped static initialization 的方式,也就是本文提到的 Meyers Singleton 實作方式,https://github.com/aosp-mirror/platform_system_core/blob/34a0e57a257f0081c672c9be0e87230762e677ca/libutils/include/utils/Singleton.h libutils/include/utils/Singleton.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 template <typename TYPE>class ANDROID_API Singleton { public : static TYPE& getInstance () { Mutex::Autolock _l(sLock); TYPE* instance = sInstance; if (instance == nullptr ) { instance = new TYPE(); sInstance = instance; } return *instance; } protected : ~Singleton() { } Singleton() { } private : static Mutex sLock; static TYPE* sInstance; };
知名的 xbmc 專案(現已改名為 Kodi)也是採用 Meyers Singleton 實作方式 xbmc CPlayerCoreFactory::GetInstance()https://github.com/xbmc/xbmc/blob/f621a026f671ab0f1a91d411f2f452915242bddf/xbmc/cores/playercorefactory/PlayerCoreFactory.cpp xbmc/cores/playercorefactory/PlayerCoreFactory.cpp 1 2 3 4 5 CPlayerCoreFactory& CPlayerCoreFactory::GetInstance() { static CPlayerCoreFactory sPlayerCoreFactory; return sPlayerCoreFactory; }
參考 單例模式 - wikipediahttps://zh.wikipedia.org/wiki/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F [ Day 5 ] 初探設計模式 - 單例模式 (Singleton)https://ithelp.ithome.com.tw/articles/10203092 Java單例模式——並非看起來那麼簡單https://blog.csdn.net/goodlixueyong/article/details/51935526 Singleton class and correct way to access it in C++https://codereview.stackexchange.com/questions/197486/singleton-class-and-correct-way-to-access-it-in-c So Singletons are bad, then what?https://softwareengineering.stackexchange.com/questions/40373/so-singletons-are-bad-then-what/40374#40374 It’s important to distinguish here between single instances and the Singleton design pattern. C++ Singleton design patternhttps://stackoverflow.com/questions/1008019/c-singleton-design-pattern/1008289#1008289 Singleton pattern in C++https://stackoverflow.com/questions/2496918/singleton-pattern-in-c
其它相關文章推薦 C/C++ 新手入門教學懶人包