C++11實現執行緒安全單例(二)
相信很多小夥伴,對單例模式很熟悉,但是對於選擇哪一種單例模式方案,可能不是特別清楚。
對網上五花百門的實現方式,是不是覺得很頭大,到底這些方案都有些啥缺點,啥優點,哪種最完美,可以作為自己的常用程式碼庫。
如果有耐心,請仔細閱讀下文,帶你回顧一下程式設計師對於單例模式實現方案的辛酸歷程。
沒有耐心的話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
懶漢模式程式碼:
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
到此,餓漢模式就是最終版本了,後續不會對它進行改造了。
餓漢模式的缺點:也是比較明顯,不使用卻佔用資源,不支援向單例建構函式傳參;
優點:就是節省了執行時間(資源提前載入),另外天生自帶執行緒安全屬性(在多執行緒環境下肯定是執行緒安全的,因為不存在多執行緒例項化的問題)。
(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
if (!_instance)
{
_instance = new T();
}
return _instance;
}
private:
static T* _instance;
static std::mutex m_cs;
};
template
T* Singleton
template
std::mutex Singleton
似乎解決了多執行緒例項化安全性問題,完美?
但是似乎引出了其他的問題,我們在每次呼叫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
if (!_instance)
{
_instance = new T();
}
}
return _instance;
}
private:
static T* _instance;
static std::mutex m_cs;
};
template
T* Singleton
template
std::mutex Singleton
這樣就安全了嗎。細想下其實還是不安全的。
注意到_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
template
std::once_flag Singleton
之前我們的單例,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
});
return _instance;
}
private:
static T* _instance;
static std::once_flag _flag;
};
template
T* Singleton
template
std::once_flag Singleton
一般而言,單例物件無需手動釋放,程式結束後,由作業系統自動回收資源。但是為了某些時候特殊處理,我們還是新增上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
});
return _instance;
}
static void destroy()
{
if (_instance)
{
delete _instance;
_instance = NULL;
}
}
private:
static T* _instance;
static std::once_flag _flag;
};
template
T* Singleton
template
std::once_flag Singleton
到此,這裡為懶漢模式的最終版,我們姑且稱之為懶漢版本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
Keyboard* t2 = Singleton
t1->writeWords();
t2->writeWords();
Singleton
QCoreApplication a(argc, argv);
return a。exec();
}
結果:
原文:使用C++11實現執行緒安全的單例模式_百里楊的部落格-CSDN部落格