您當前的位置:首頁 > 舞蹈

C++11實現執行緒安全單例(二)

作者:由 Mason 發表于 舞蹈時間:2021-02-23

相信很多小夥伴,對單例模式很熟悉,但是對於選擇哪一種單例模式方案,可能不是特別清楚。

對網上五花百門的實現方式,是不是覺得很頭大,到底這些方案都有些啥缺點,啥優點,哪種最完美,可以作為自己的常用程式碼庫。

如果有耐心,請仔細閱讀下文,帶你回顧一下程式設計師對於單例模式實現方案的辛酸歷程。

沒有耐心的話o(*^@^*)o,請直接跳到《C++11實現執行緒安全單例》章節,你將得到一份完美的多執行緒安全單例模式程式碼。

後面我會把完整程式碼貼出來,供下載。

1、C++11前,程式設計師們是怎麼實現單例模式?

(1)懶漢模式與餓漢模式

首先來了解一下懶漢模式與餓漢模式。

懶漢模式,顧名思義,就是比較懶惰,不是今天必須乾的事,堅決放到明天來完成,帶有延遲載入的意思。

餓漢模式,意思就是餓了的流浪漢,這種流浪漢什麼事情都可以乾的,但凡路邊的垃圾,街上的妹紙,他都可以吃得下去,飢不擇食;

軟體一起來,就儘早吃記憶體,帶有提前載入的意思(哪怕暫時用不到)。

我們舉一個栗子,比如鍵盤,一個系統正常輸入,我只需要一個鍵盤,就可以了,所以鍵盤設計為一個單例,有個打字方法writeWords()。

餓漢模式程式碼:

class Keyboard

{

public:

Keyboard() {}

~Keyboard() {}

static Keyboard* instance()

{

return _pInstance;

}

void writeWords() { }

private:

static Keyboard* _pInstance;

};

Keyboard* Keyboard::_pInstance = new Keyboard();

懶漢模式程式碼:

class Keyboard

{

public:

Keyboard() {}

~Keyboard() {}

static Keyboard* instance()

{

if (!_pInstance)

{

_pInstance = new Keyboard();

}

return _pInstance;

}

void writeWords() { }

private:

static Keyboard* _pInstance;

};

Keyboard* Keyboard::_pInstance = NULL;

(2)禁止建構函式、複製構造與賦值函式

既然是單例,肯定不允許外面呼叫建構函式例項化新物件;也不允許複製間接例項化新物件;也不允許物件賦值。

我們改造下上面的懶漢與餓漢。

餓漢模式程式碼:

class Keyboard

{

private:

Keyboard() = default;

~Keyboard() = default;

Keyboard(const Keyboard&)=delete;

Keyboard& operator=(const Keyboard&)=delete;

public:

static Keyboard* instance()

{

return _pInstance;

}

void writeWords() { }

private:

static Keyboard* _pInstance;

};

Keyboard* Keyboard::_pInstance = new Keyboard();

懶漢模式程式碼:

class Keyboard

{

private:

Keyboard() = default;

~Keyboard() = default;

Keyboard(const Keyboard&)=delete;

Keyboard& operator=(const Keyboard&)=delete;

public:

static Keyboard* instance()

{

if (!_pInstance)

{

_pInstance = new Keyboard();

}

return _pInstance;

}

void writeWords() { }

private:

static Keyboard* _pInstance;

};

Keyboard* Keyboard::_pInstance = NULL;

(3)單例的模板化

現在我們再來舉個栗子,現在已有一個鍵盤單例了,像這樣的單例,我們還需要很多個,比如滑鼠、顯示器、耳機。。。(請忽略合理性)。

假設我們選擇懶漢模式,那麼我們還需要分別新增Mouse、Displayer、Headset三個類,且其實現單例功能的程式碼和Keyboard高度類似。

發現了嗎,小夥伴,我們在做重複性工作了,so,我們需要將單例類模板化,這樣每個不同的類需要實現單例,可以套用一個模板。

餓漢模式程式碼:

template

class Singleton

{

private:

Singleton() = default;

~Singleton() = default;

Singleton(const Singleton&)=delete;

Singleton& operator=(const Singleton&)=delete;

public:

static T* instance() { return _instance; }

private:

static T* _instance;

};

template

T* Singleton::_instance = new T();

懶漢模式程式碼:

template

class Singleton

{

private:

Singleton() = default;

~Singleton() = default;

Singleton(const Singleton&)=delete;

Singleton& operator=(const Singleton&)=delete;

public:

static T* instance()

{

if (!_instance)

{

_instance = new T();

}

return _instance;

}

private:

static T* _instance;

};

template

T* Singleton::_instance = NULL;

到此,餓漢模式就是最終版本了,後續不會對它進行改造了。

餓漢模式的缺點:也是比較明顯,不使用卻佔用資源,不支援向單例建構函式傳參;

優點:就是節省了執行時間(資源提前載入),另外天生自帶執行緒安全屬性(在多執行緒環境下肯定是執行緒安全的,因為不存在多執行緒例項化的問題)。

(4)懶漢模式之執行緒安全性探索

a。懶漢模式下,在定義_instance 變數時先等於NULL,在呼叫instance()方法時,再判斷是否要賦值。這種模式,並非是執行緒安全的,因為多個執行緒同時呼叫instance()方法,就可能導致有產生多個例項。比如A執行緒執行到第13行之後,第15行之前,當前_instance==NULL,此時由於執行緒排程,切到B執行緒,B執行緒發現_instance==NULL,則進入new T()進行例項化,例項化完成,返回物件指標,然後某一刻發生執行緒排程,切回到A執行緒,A執行緒從以前被打斷的地方繼續執行,發現_instance==NULL,則進入new T()進行例項化,這樣就出現了2個單例物件。顯然這是執行緒非安全的。

那麼,要實現執行緒安全,就必須加鎖,以保證物件例項化的原子性。

改造後的程式碼:

template

class Singleton

{

private:

Singleton() = default;

~Singleton() = default;

Singleton(const Singleton&)=delete;

Singleton& operator=(const Singleton&)=delete;

public:

static T* instance()

{

std::lock_guard lock_(m_cs);

if (!_instance)

{

_instance = new T();

}

return _instance;

}

private:

static T* _instance;

static std::mutex m_cs;

};

template

T* Singleton::_instance = NULL;

template

std::mutex Singleton::m_cs;

似乎解決了多執行緒例項化安全性問題,完美?

但是似乎引出了其他的問題,我們在每次呼叫instance()時,都會呼叫進一次加/解鎖,但是實際上這個鎖只在我們第一次建立物件時,用來防止多執行緒競爭起到作用。物件建立起來後,多執行緒都是讀取操作,沒有寫入操作,所以就不會有安全性問題,此後的呼叫,我們無疑浪費了很多資源。

b。此時我們需要引出一個高大上的名稱:DCLP(Double-Checked Locking Pattern),即“雙檢鎖”。怎麼操作?就是加個if判斷。

改造後的程式碼:

template

class Singleton

{

private:

Singleton() = default;

~Singleton() = default;

Singleton(const Singleton&)=delete;

Singleton& operator=(const Singleton&)=delete;

public:

static T* instance()

{

if (!_instance)

{

std::lock_guard lock_(m_cs);

if (!_instance)

{

_instance = new T();

}

}

return _instance;

}

private:

static T* _instance;

static std::mutex m_cs;

};

template

T* Singleton::_instance = NULL;

template

std::mutex Singleton::m_cs;

這樣就安全了嗎。細想下其實還是不安全的。

注意到_instance = new T(),是一個寫操作,前面有一個無鎖的讀操作。當真正的寫操作進行時,前面的讀操作存在髒讀情況。

另外其他原因:

https://

blog。csdn。net/flyingleo

1981/article/details/45485293?depth_1-utm_source=distribute。pc_relevant。none-task-blog-BlogCommendFromBaidu-1&utm_source=distribute。pc_relevant。none-task-blog-BlogCommendFromBaidu-1

這對於碼農來說,經過一番折騰,然而卻沒有得到一個完美的解決方案,這是殘忍的。。。

那麼,有沒有執行緒安全的懶漢模式單例實現方案呢?答案是有,還好有你(C++11)。下一節介紹C++11實現執行緒安全單例。

2、C++11實現執行緒安全單例(懶漢)

在C++11中提供一種方法,使得函式可以執行緒安全的只調用一次。即使用std::call_once和std::once_flag。std::call_once是一種lazy load的很簡單易用的機制。

懶漢模式改造後的程式碼:

template

class Singleton

{

private:

Singleton() = default;

~Singleton() = default;

Singleton(const Singleton&)=delete;

Singleton& operator=(const Singleton&)=delete;

public:

static T* instance()

{

std::call_once(_flag, [&](){

_instance = new T();

});

return _instance;

}

private:

static T* _instance;

static std::once_flag _flag;

};

template

T* Singleton::_instance = NULL;

template

std::once_flag Singleton::_flag;

之前我們的單例,instance()只能建立預設建構函式的物件,但是有時候需要給單例傳遞引數,那麼我們需要對instance()方法進行改造,在c++11中,已經支援了可變引數函式。

然而向單例建構函式中傳參,這個需求,餓漢模式就無法實現了。

下面我們繼續改造,新增建構函式傳參。

懶漢模式改造後的程式碼:

template

class Singleton

{

private:

Singleton() = default;

~Singleton() = default;

Singleton(const Singleton&)=delete;

Singleton& operator=(const Singleton&)=delete;

public:

template

static T* instance(Args&&。。。 args)

{

std::call_once(_flag, &](){

_instance = new T(std::forward(args)。。。);

});

return _instance;

}

private:

static T* _instance;

static std::once_flag _flag;

};

template

T* Singleton::_instance = NULL;

template

std::once_flag Singleton::_flag;

一般而言,單例物件無需手動釋放,程式結束後,由作業系統自動回收資源。但是為了某些時候特殊處理,我們還是新增上destroy()方法。

改造後的程式碼:

template

class Singleton

{

private:

Singleton() = default;

~Singleton() = default;

Singleton(const Singleton&)=delete;

Singleton& operator=(const Singleton&)=delete;

public:

template

static T* instance(Args&&。。。 args)

{

std::call_once(_flag, [&](){

_instance = new T(std::forward(args)。。。);

});

return _instance;

}

static void destroy()

{

if (_instance)

{

delete _instance;

_instance = NULL;

}

}

private:

static T* _instance;

static std::once_flag _flag;

};

template

T* Singleton::_instance = NULL;

template

std::once_flag Singleton::_flag;

到此,這裡為懶漢模式的最終版,我們姑且稱之為懶漢版本1。

在C++11標準中,要求區域性靜態變數初始化具有執行緒安全性。

另外還有一個版本的懶漢模式程式碼,也是支援執行緒安全(開啟編譯器C++11支援),大家看看,大概長這樣:

class Singleton

{

public:

static Singleton* instance()

{

static Singleton _instance;

return &s_instance;

}

private:

Singleton() {}

};

這裡使用了局部靜態變數,C++11機制可以保證它的初始化具有原子性,執行緒安全。

這個物件儲存在靜態資料區,和全域性變數是在一起的,而不是在堆中。

習慣讓我感覺物件儲存在堆中更好,至於是不是,待解釋。

姑且稱這個為懶漢版本2。

3、結論(拿乾貨)

經過上面的角逐,現在剩下3位選手:餓漢最終版、懶漢版本1、懶漢版本2。

餓漢與懶漢陣營PK:餓漢資源提前載入,浪費比較嚴重,尤其有一些功能,使用者可以選擇性啟用的,使用者如果不需要,

犯不著一上來就佔用額外資源;懶漢不存在資源浪費,且同時具備執行緒安全。

第一局:懶漢勝利,選擇懶漢。

個人覺得還是推薦使用懶漢模式,支援執行緒安全,建構函式傳參,手動回收資源。

第二局:懶漢版本1 PK 版本2。

套用上面一句話:習慣讓我感覺物件儲存在堆中更好,至於是不是,待解釋。

所以我們選擇懶漢版本1。真是玩笑了,O(∩_∩)O哈哈~

具體用哪個懶漢,看個人喜好吧。

我個人比較傾向於懶漢版本1。

下面是測試程式碼(main。cpp):

#include

#include

#include “singleton。h”

class Keyboard

{

public:

Keyboard(int a = 0, float b = 0。0)

{

std::cout << “Keyboard():” << (a+b) << std::endl;

}

~Keyboard()

{

std::cout << “~Keyboard()” << std::endl;

}

void writeWords()

{

std::cout << “I‘m writing! addr : ” << (int)this << std::endl;

}

};

int main(int argc, char *argv[])

{

Keyboard* t1 = Singleton::instance(5, 2。0);

Keyboard* t2 = Singleton::instance(6, 5。0);

t1->writeWords();

t2->writeWords();

Singleton::destroy();

QCoreApplication a(argc, argv);

return a。exec();

}

結果:

C++11實現執行緒安全單例(二)

原文:使用C++11實現執行緒安全的單例模式_百里楊的部落格-CSDN部落格

標簽: singleton  instance  Keyboard  static  懶漢