設計模式之單例模式(c++版)

動機

保證一個類僅有一個實例,並提供一個該實例的全局訪問點。 ——《設計模式》GoF

在軟件系統中,常常有這樣一些特殊的類,必須保證他們在系統中只存在一個實例,才能確保它們的邏輯正確性、以及良好的效率。html

因此得考慮如何繞過常規的構造器(不容許使用者new出一個對象),提供一種機制來保證一個類只有一個實例。java

應用場景:linux

  • Windows的Task Manager(任務管理器)就是很典型的單例模式,你不能同時打開兩個任務管理器。Windows的回收站也是同理。
  • 應用程序的日誌應用,通常均可以用單例模式實現,只能有一個實例去操做文件。
  • 讀取配置文件,讀取的配置項是公有的,一個地方讀取了全部地方都能用,沒有必要全部的地方都能讀取一遍配置。
  • 數據庫鏈接池,多線程的線程池。

實現一[線程不安全版本]

class Singleton{
public:
    static Singleton* getInstance(){
        // 先檢查對象是否存在
        if (m_instance == nullptr) {
            m_instance = new Singleton();
        }
        return m_instance;
    }
private:
    Singleton(); //私有構造函數,不容許使用者本身生成對象
    Singleton(const Singleton& other);
    static Singleton* m_instance; //靜態成員變量 
};

Singleton* Singleton::m_instance=nullptr; //靜態成員須要先初始化

這是單例模式最經典的實現方式,將構造函數和拷貝構造函數都設爲私有的,並且採用了延遲初始化的方式,在第一次調用getInstance()的時候纔會生成對象,不調用就不會生成對象,不佔據內存。然而,在多線程的狀況下,這種方法是不安全的。c++

分析:正常狀況下,若是線程A調用getInstance()時,將m_instance 初始化了,那麼線程B再調用getInstance()時,就不會再執行new了,直接返回以前構造好的對象。然而存在這種狀況,線程A執行m_instance = new Singleton()還沒完成,這個時候m_instance仍然爲nullptr,線程B也正在執行m_instance = new Singleton(),這是就會產生兩個對象,線程AB可能使用的是同一個對象,也多是兩個對象,這樣就可能致使程序錯誤,同時,還會發生內存泄漏。數據庫

實現二[線程安全,鎖的代價太高]

//線程安全版本,但鎖的代價太高
Singleton* Singleton::getInstance() {
    Lock lock; //僞代碼 加鎖
    if (m_instance == nullptr) {
        m_instance = new Singleton();
    }
    return m_instance;
}

分析:這種寫法不會出現上面兩個線程都執行new的狀況,當線程A在執行m_instance = new Singleton()的時候,線程B若是調用了getInstance(),必定會被阻塞在加鎖處,等待線程A執行結束後釋放這個鎖。從而是線程安全的。編程

但這種寫法的性能不高,由於每次調用getInstance()都會加鎖釋放鎖,而這個步驟只有在第一次new Singleton()纔是有必要的,只要m_instance被建立出來了,無論多少線程同時訪問,使用if (m_instance == nullptr) 進行判斷都是足夠的(只是讀操做,不須要加鎖),沒有線程安全問題,加了鎖以後反而存在性能問題。c#

實現三[雙檢查鎖,因爲內存讀寫reoder致使不安全]

上面的作法是無論三七二十一,某個線程要訪問的時候,先鎖上再說,這樣會致使沒必要要的鎖的消耗,那麼,是否能夠先判斷下if (m_instance == nullptr) 呢,若是知足,說明根本不須要鎖啊!這就是所謂的雙檢查鎖(DCL)的思想,DCL即double-checked locking。設計模式

//雙檢查鎖,但因爲內存讀寫reorder不安全
Singleton* Singleton::getInstance() {
    //先判斷是否是初始化了,若是初始化過,就不再會使用鎖了
    if(m_instance==nullptr){
        Lock lock; //僞代碼
        if (m_instance == nullptr) {
            m_instance = new Singleton();
        }
    }
    return m_instance;
}

這樣看起來很棒!只有在第一次必要的時候纔會使用鎖,以後就和實現一中同樣了。安全

在至關長的一段時間,迷惑了不少人,在2000年的時候才被人發現漏洞,並且在每種語言上都發現了。緣由是內存讀寫的亂序執行(編譯器的問題)。多線程

分析:m_instance = new Singleton()這句話能夠分紅三個步驟來執行:

  1. 分配了一個Singleton類型對象所須要的內存。
  2. 在分配的內存處構造Singleton類型的對象。
  3. 把分配的內存的地址賦給指針m_instance

可能會認爲這三個步驟是按順序執行的,但實際上只能肯定步驟1是最早執行的,步驟23卻不必定。問題就出如今這。假如某個線程A在調用執行m_instance = new Singleton()的時候是按照1,3,2的順序的,那麼剛剛執行完步驟3Singleton類型分配了內存(此時m_instance就不是nullptr了)就切換到了線程B,因爲m_instance已經不是nullptr了,因此線程B會直接執行return m_instance獲得一個對象,而這個對象並無真正的被構造!!嚴重bug就這麼發生了。

實現四[C++ 11版本的跨平臺實現]

javac#發現這個問題後,就加了一個關鍵字volatile,在聲明m_instance變量的時候,要加上volatile修飾,編譯器看到以後,就知道這個地方不可以reorder(必定要先分配內存,在執行構造器,都完成以後再賦值)。

而對於c++標準卻一直沒有改正,因此VC++2005版本也加入了這個關鍵字,可是這並不可以跨平臺(只支持微軟平臺)。

而到了c++ 11版本,終於有了這樣的機制幫助咱們實現跨平臺的方案。

//C++ 11版本以後的跨平臺實現 
// atomic c++11中提供的原子操做
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;

/*
* std::atomic_thread_fence(std::memory_order_acquire); 
* std::atomic_thread_fence(std::memory_order_release);
* 這兩句話能夠保證他們之間的語句不會發生亂序執行。
*/
Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire);//獲取內存fence
    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);//釋放內存fence
            m_instance.store(tmp, std::memory_order_relaxed);
        }
    }
    return tmp;
}

實現五[pthread_once函數]

在linux中,pthread_once()函數能夠保證某個函數只執行一次。

聲明: int pthread_once(pthread_once_t once_control, void (init_routine) (void));

功能: 本函數使用初值爲PTHREAD_ONCE_INIT的once_control
變量保證init_routine()函數在本進程執行序列中僅執行一次。

示例以下:

class Singleton{
public:
    static Singleton* getInstance(){
        // init函數只會執行一次
        pthread_once(&ponce_, &Singleton::init);
        return m_instance;
    }
private:
    Singleton(); //私有構造函數,不容許使用者本身生成對象
    Singleton(const Singleton& other);
    //要寫成靜態方法的緣由:類成員函數隱含傳遞this指針(第一個參數)
    static void init() {
        m_instance = new Singleton();
      }
    static pthread_once_t ponce_;
    static Singleton* m_instance; //靜態成員變量 
};
pthread_once_t Singleton::ponce_ = PTHREAD_ONCE_INIT;
Singleton* Singleton::m_instance=nullptr; //靜態成員須要先初始化

實現六[c++ 11版本最簡潔的跨平臺方案]

實現四的方案有點麻煩,實現五的方案不能跨平臺。其實c++ 11中已經提供了std::call_once方法來保證函數在多線程環境中只被調用一次,一樣,他也須要一個once_flag的參數。用法和pthread_once相似,而且支持跨平臺。

實際上,還有一種最爲簡單的方案!

在C++memory model中對static local variable,說道:The initialization of such a variable is defined to occur the first time control passes through its declaration; for multiple threads calling the function, this means there’s the potential for a race condition to define first.

局部靜態變量不只只會初始化一次,並且仍是線程安全的。

class Singleton{
public:
    // 注意返回的是引用。
    static Singleton& getInstance(){
        static Singleton m_instance;  //局部靜態變量
        return m_instance;
    }
private:
    Singleton(); //私有構造函數,不容許使用者本身生成對象
    Singleton(const Singleton& other);
};

這種單例被稱爲Meyers' Singleton 。這種方法很簡潔,也很完美,可是注意:

  1. gcc 4.0以後的編譯器支持這種寫法。
  2. C++11及之後的版本(如C++14)的多線程下,正確。
  3. C++11以前不能這麼寫。

可是如今都18年了。。新項目通常都支持了c++11了。

用模板包裝單例

從上面已經知道了單例模式的各類實現方式。可是有沒有感到一點不和諧的地方?若是我class A須要作成單例,須要這麼改造class A,若是class B也須要作成單例,仍是須要這樣改造一番,是否是有點重複勞動的感受?利用c++的模板語法能夠避免這樣的重複勞動。

template<typename T>
class Singleton
{
public:
    static T& getInstance() {
        static T value_; //靜態局部變量
        return value_;
    }

private:
    Singleton();
    ~Singleton();
    Singleton(const Singleton&); //拷貝構造函數
    Singleton& operator=(const Singleton&); // =運算符重載
};

假若有AB兩個類,用Singleton類能夠很容易的把他們也包裝成單例。

class A{
public:
    A(){
       a = 1;
    }
    void func(){
        cout << "A.a = " << a << endl;
    }

private:
    int a;
};

class B{
public:
    B(){
        b = 2;
    }

    void func(){
        cout << "B.b = " << b << endl;
    }
private:
    int b;
};

// 使用demo
int main()
{
    Singleton<A>::getInstance().func();
    Singleton<B>::getInstance().func();
    return 0;
}

假如類A的構造函數具備參數呢?上面的寫法仍是沒有通用性。可使用C++11的可變參數模板解決這個問題。可是感受實際中這種需求並非不少,由於構造只須要一次,每次getInstance()傳個參數不是很麻煩嗎。。。

總結

單例模式自己十分簡單,可是實現上卻發現各類麻煩,主要是多線程編程確實是個難點。而對於c++的對象模型、內存模型,並無什麼深刻的瞭解,還在只知其一;不知其二的階段,仍需努力。

須要注意的一點是,上面討論的線程安全指的是getInstance()是線程安全的,假如多個線程都獲取類A的對象,若是隻是隻讀操做,徹底OK,可是若是有線程要修改,有線程要讀取,那麼類A自身的函數須要本身加鎖防禦,不是說線程安全的單例也能保證修改和讀取該對象自身的資源也是線程安全的。

個人簡書連接

參考

  1. C++中多線程與Singleton的那些事兒
  2. boolan c++設計模式
相關文章
相關標籤/搜索