C++ 設計模式 - 單例模式 Singleton Pattern

本篇介紹 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
// g++ cpp-singleton-hungry.cpp -std=c++11 -pthread
#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
// g++ cpp-singleton-lazy2.cpp -std=c++11 -pthread
#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

雙重檢測上鎖版 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
// g++ cpp-singleton-lock.cpp -std=c++11 -pthread
#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
// g++ cpp-singleton-lock2.cpp -std=c++11 -pthread
#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 // Step 3
= operator new(sizeof(Singleton)); // Step 1
new(sInstance) Singleton; // Step 2
}
}
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
// g++ cpp-singleton-meyers.cpp -std=c++11 -pthread
#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
// g++ cpp-singleton.cpp -std=c++11
#include <iostream>
using namespace std;

class Setting {
public:
static Setting& getInstance() {
static Setting instance;
return instance;
}

Setting(Setting const&) = delete;
// Setting s;
// Setting s2(s); // x

void operator=(Setting const&) = delete;
// Setting s2;
// s2 = s; // x

private:
Setting() {}
// Setting s; // x
};

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_ptr
https://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
// DO NOT USE: Please use scoped static initialization. For instance:
// MyClass& getInstance() {
// static MyClass gInstance(...);
// return gInstance;
// }
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;
}

參考
單例模式 - wikipedia
https://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 pattern
https://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++ 新手入門教學懶人包