1. 程式人生 > >設計模式之單例模式(C++)

設計模式之單例模式(C++)

設計模式之單例模式(C++實現)

1. 什麼是Singleton?

設計模式中的Singleton,中文翻譯是單例模式(也有翻譯單件模式),指的是限制一個類只能例項化出一個物件。這種看是奇特使用類的方式在某些情況下是有用的,比如整個系統中只有一個狀態管理的類,又比如在Windows中的工作管理員。這些應用場景下只需要一個類的例項,多個例項反而不方便管理,也可能存在某些潛在的衝突,在這些應用中Singleton就可以很好地得到應用。

2. 替代方案

Singleton很明顯可以使用一個全域性的變數來替換(C++),但是相比於全域性變數它有以下的優勢:

可以避免引入與全域性變數產生衝突
使用單例模式可以使用到延遲載入(lazy allocation and initialization),在沒有使用的時候可以節約資源

3. C++中實現

單例模式的一種常用實現如下:

1)考慮到需要只有一個例項,因此常常使用一個靜態的變數來表示;
2)為了避免類的使用者new出多個例項,需要將建構函式宣告為private
3)但是需要有一種可以得到這個物件的方式,一般使用getInstatnce()這個public的方法來獲取這個例項

綜合上述介紹,一個簡單的實現如下 1.

//定義
class Singleton {
public:
    static Singleton* getInstance();
    void doSomething();
protected:
    Singleton();
private
: static Singleton* _instance; }; //實現 Singleton* Singleton::_instance = nullptr; Singleton* Singleton::getInstance() { if (_instance == nullptr) { _instance = new Singleton; } return _instance; } void Singleton::doSomething() { std::cout << "Doing" << std::endl; }

這個實現在單執行緒的環境下工作的很好,但是在多執行緒環境中可能存在著潛在的危險,考慮到兩個執行緒同時執行到 if (_instance == nullptr),其中一個執行緒在這個時候被掛起,當另一個執行緒初始化_instance之後,喚醒掛起的執行緒,這時候該執行緒會繼續建立一個例項,這與Singleton中單個例項的設計背道而馳了。

4. 解決方案

4.1 簡單實現

既然上述的設計並非是執行緒安全的,那麼我們在構造_instance的時候給它加鎖不就好了嗎?實現如下:

//定義
class Singleton {
public:
    static Singleton* getInstance();
    void doSomething();
protected:
    Singleton();
private:
    static Singleton* _instance;
};

//實現
Singleton* Singleton::_instance = nullptr;
std::mutex mutex;

Singleton* Singleton::getInstance() {
    // 加上mutex使得每次只有一個執行緒進入
    std::lock_guard<std::mutex> locker(mutex);
    if (_instance == nullptr) {
        _instance = new Singleton;
    }
    return _instance;
}

void Singleton::doSomething() {
    std::cout << "Doing" << std::endl;
}

這樣又會引入一個新的令人不爽的地方,執行緒在getInstance呼叫時都需要等待其他執行緒完成訪問,這樣不利於多執行緒發揮出它應有的優勢。仔細看一下實現,發現實際上真正需要加鎖的只有new這一個步驟,這樣在初始化完成之後所有的執行緒就不會在進入到if這個分支中了,於是我們修改為:

Singleton* Singleton::getInstance() {

    if (_instance == nullptr) {
        // 加上mutex使得每次只有一個執行緒進入
    std::lock_guard<std::mutex> locker(mutex);
        _instance = new Singleton;
    }
    return _instance;
}

但是這樣做有一個潛在的危險,當執行緒A和B都執行到if(_instance == nullptr)之後A執行緒掛起,B執行緒執行到加鎖,new出物件,之後A執行緒被喚醒,它也會加鎖並再次建立一個物件,於是又會出現兩個例項,這與單例模式相悖。

4.2 DCLP方式

既然還是會建立新的物件,那麼我們在建立之前再次判斷一下不就好了嗎,於是引出新的一個概念稱為 The Double-Checked Locking Pattern(DCL
P),具體實現如下:

Singleton* Singleton::getInstance() {

    if (_instance == nullptr) {
        // 加上mutex使得每次只有一個執行緒進入
    std::lock_guard<std::mutex> locker(mutex);
       if (_instance == nullptr)
         _instance = new Singleton;
    }
    return _instance;
}

接著上文的敘述,在A執行緒加鎖之後發現B執行緒已經將_instance例項化了,於是_instance == nullptr為false,這樣A就不會再次new出例項了,完美的解決了問題。

4.3 新的麻煩

使用DCLP就可以解決這個問題嗎?事實上在某種程度上可以,但是不同的編譯器在執行getInstance這個函式的時候有不同的處理方式,使得使用DCLP並不是一個適用於所有平臺和編譯器的完美解決方案,具體的細節十分複雜,讀者可以參考下面這篇論文
C++ and the Perils of Double-Checked Locking2
文中給出了一個實現真正執行緒安全的一個實現(需要針對不同的作業系統實現),實現的模式是:

Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance;
    ...                     // insert memory barrier
    if (tmp == NULL) {
        Lock lock;
        tmp = m_instance;
        if (tmp == NULL) {
            tmp = new Singleton;
            ...             // insert memory barrier
            m_instance = tmp;
        }
    }
    return tmp;
}

4.4 C++11的實現

C++11的一大亮點是引入了跨平臺的執行緒庫,通過執行緒庫可以很好的完成上文中提到的問題。

4.4.1 實現方式一

這種實現方式實際上就是對上文中的DCLP的描述,實現程式碼如下:

std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;

Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire);
    if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new Singleton;
            std::atomic_thread_fence(std::memory_order_release);
            m_instance.store(tmp, std::memory_order_relaxed);
        }
    }
    return tmp;
}

另外還有其他的實現方式,更多的內容可以參考:Double-Checked Locking Is Fixed In C++11 3

4.4.2 推薦的實現方式

對於singleton的實現方式,在Effective C++中作者實現了

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

        S(S const&)               = delete;
        void operator=(S const&)  = delete;

};

這種實現方式是執行緒安全的(C++11),在C++11的規範中有一下描述:

The 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.

也就是說C++11保證了靜態變數在被初始化的時候強制執行緒保持同步,這種方式就實現了無鎖完美的方案,具體內部的實現是編譯器的事情,它可能使用DCLP的方式或者其他的方式完成。但是C++11提到的這個語義保證了單例模式的實現。
更多的內容可以參考4