0%

Singleton implementation and discussion in C++

Singleton這個題目酸燃被出到爛了,不過變化實在千變萬化。列出幾種不錯的解決方式。

Meyers version

1
2
3
4
5
6
7
8
9
class Singleton {
private:
Singleton() {}
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
};

非常有名的實作方式,在C++11的環境下是Thread-safe的,而C++98沒有這種保證。可以參考Is Meyers implementation of Singleton pattern thread safe?這個討論串。
GCC預設編譯時開啟static threadsafe的選項,所以C++11的程式碼可以正確運行,可以強制使用-fno-threadsafe-statics關閉這功能。可以參考Are function static variables thread-safe in GCC?

call_once and once_flag

C++11新增的call_once功能可以保證函數只執行一次,因此可以拿來用在Singleton的建立上。libuve也有類似的thread-safe callonce功能。C語言如果有需要的話可以從那邊找答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <mutex>
class Singleton {
private:
static std::unique_ptr<Singleton> instance;
static std::once_flag initInstanceFlag;
Singleton() {}
public:
static Singleton& getInstance() {
std::call_once(initInstanceFlag, []() {
instance.reset(new Singleton());
});
return *instance;
}
};
std::once_flag Singleton::initInstanceFlag;
std::unique_ptr<Singleton> Singleton::instance;

自行打造輪子的作法

為了練功,有時自己倒造輪子可以有更深一曾的了解

Thread-safe lock version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <memory>
#include <thread>
class Singleton {
private:
static std::unique_ptr<Singleton> instance;
static std::mutex instanceMutex;
Singleton() {}
public:
static Singleton& getInstance() {
std::lock_guard<std::mutex> lock(instanceMutex);
if (!instance)
instance.reset(new Singleton());
return *instance;
}
};
std::mutex Singleton::instanceMutex;
std::unique_ptr<Singleton> Singleton::instance;

這個版本很簡單,在每個Thread呼叫getInstance時就獲取Lock,因此可以保證Constructor只會有一個Thread執行,不過這方式的缺點也很明顯,當建立完singleton之後就不需Lock了。

Double-Checked Locking

最受爭議的實作方式,原因不是在於程式碼有問題,而是Compiler & Cpu不會照程式碼所預料的方式行動。最原始寫法是這樣

1
2
3
4
5
6
7
8
Singleton& Singleton::instance() {
if (!pInstance) { // 1st test
Lock lock;
if (!pInstance) // 2nd test
pInstance.reset(new Singleton);
}
return *pInstance;
}

這個問題的原因很複雜,可以看這兩篇文章

  • The “Double-Checked Locking is Broken” Declaration
  • C++ and the Perils of Double-Checked Locking
  • Double-Checked Locking, Threads, Compiler Optimizations, and More
    根據論文最後,可以看到一個可行的解
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Singleton* Singleton::instance() {
    Singleton *tmp = pInstance;
    ... // need memory barrier
    if (tmp == 0) { // 1st test
    Lock lock;
    tmp = pInstance;
    if(tmp == 0) { // 2nd test
    tmp = new Singleton;
    ... // need memory barrier
    pInstance = tmp;
    }
    }
    return pInstance;
    }
    需要Memory barrier的部份,由於C++98對此沒有規範,所以只能靠Compiler個顯神吞。

    Double-Checked Locking revisited

    在C++11之後,引進了atomic操作。因此上面的方式變得可能。
    實際寫了一下,目前的atomic跟unique_ptr不能共用,等待N4058 - Atomic Smart Pointers哪一天能通過。現在先用Raw pointer來做。
    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
    #include <atomic>
    #include <mutex>
    #include <thread>
    class Singleton {
    private:
    static std::atomic<Singleton *> instance;
    static std::mutex instanceMutex;
    Singleton() {}
    public:
    static Singleton& getInstance() {
    Singleton* tmp = instance.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire);
    if (tmp == nullptr) {
    std::lock_guard<std::mutex> lock(instanceMutex);
    tmp = instance.load(std::memory_order_relaxed);
    if (tmp == nullptr) {
    tmp = new Singleton;
    std::atomic_thread_fence(std::memory_order_release);
    instance.store(tmp, std::memory_order_relaxed);
    }
    }
    return *tmp;
    }
    };
    std::mutex Singleton::instanceMutex;
    std::atomic<Singleton *> Singleton::instance;

以下這幾種方式是從 Binary Hacks學來的

Thread-local storage based solution

雖然這跟Singleton的定義不太相同,每個Thread擁有自己的Singleton,如此之來就不用加任何Lock了,如果對此計較的話不是用。

1
2
3
4
5
6
7
8
9
10
11
class Singleton {
private:
Singleton() {}
public:
static Singleton& getInstance() {
static thread_local std::unique_ptr<Singleton> instance;
if (!instance)
instance.reset(new Singleton());
return *instance;
}
};

Self Modification Code

運用Self Modification Code這種高等技巧,在第一次執行完getInstance()之後,就把整個程式碼寫成類似這樣的情形,不過這需要對Target Architecture相當的熟悉,又不能跨平台,只能當種炫技。不過SMC的技術有時間可以練練。

1
2
3
4
5
6
7
8
class Singleton {
private:
Singleton() {}
public:
static Singleton& getInstance() {
return *instance;
}
};

Reference