C++線程安全的單例模式

一、在GCC4.0以後的環境下:ios

#include <iostream>c++

using namespace std;
template <typename T>
class Singleton
{
public:
static T& getInstance() {
//使用局部靜態變量的缺陷就是建立和析構時的不肯定性。因爲Singleton實例會在Instance()函數被訪問時被建立,所以在某處新添加的一處對Singleton的訪問將可能致使Singleton的生存期發生變化。若是其依賴於其它組成,如另外一個Singleton,那麼對它們的生存期進行管理將成爲一個災難。
//甚至能夠說,還不如不用Singleton,而使用明確的實例生存期管理。」
// Lock(); GCC 4.0以上的編譯器保證了內部靜態變量的線程安全,能夠不須要這句話
//爲何c++0X以前須要加Lock,這是由局部靜態變量的實際實現所決定的。
//爲了能知足局部靜態變量只被初始化一次的需求
//,不少編譯器會經過一個全局的標誌位記錄該靜態變量是否已經被初始化的信息。
//那麼,對靜態變量進行初始化的僞碼就變成下面這個樣子:
//bool flag = false;
//if (!flag)
// {
// flag = true;
// staticVar = initStatic();
// }
static T s;
// UnLock()
cout << "new s" << endl;
return s; //若是返回的是指針可能會有被外部調用者delete掉的隱患,因此這裏返回引用會更加保險一些。
}
private:
Singleton() {}
~Singleton(){}
Singleton(const Singleton& other) {}
Singleton& operator = (const Singleton& other) {}
};面試

class Eager_Singleton //餓漢模式
{
private:
Eager_Singleton() {
}
~Eager_Singleton(){
}
Eager_Singleton(const Eager_Singleton& other);
Eager_Singleton& operator = (const Eager_Singleton& other);
private:
static Eager_Singleton s; //在程序開始時進入主函數以前就由主線程以單線程方式執行該語句完成了初始化,
public:
static Eager_Singleton& getInstance() {
return s;
}
};安全

//將Singleton做爲一個組件供其餘類使用
class SingletonInstance : public Singleton<SingletonInstance> {

};
int main() {
Singleton<Singleton>::getInstance();
Eager_Singleton::getInstance();
return 0;
}多線程

二、在GCC4.0以前,不用鎖實現函數

template<typename T>
class Singleton : boost::noncopyable
{
public:
static T& instance()
{
pthread_once(&ponce_, &Singleton::init);
return *value_;
}優化

static void init()
{
value_ = new T();
}
private:
static pthread_once_t ponce_;
static T* value_;
};
template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;spa

template<typename T>
T* Singleton<T>::value_ = NULL; 操作系統

三、在GCC4.0以前,用鎖實現(轉自write pattern line by line的一個面試場景)線程

template <typename T>
class Singleton
{
public:
static T& Instance()
{
if (m_pInstance == NULL)
{
Lock lock;
if (m_pInstance == NULL)
{
m_pInstance = new T();
Destroy();
}
return *m_pInstance;
}
return *m_pInstance;
}

protected:
Singleton(void) {}
~Singleton(void) {}

private:
Singleton(const Singleton& rhs) {}
Singleton& operator = (const Singleton& rhs) {}

void Destroy()
{
if (m_pInstance != NULL)
delete m_pInstance;
m_pInstance = NULL;
}

static T* volatile m_pInstance;
};

template <typename T>
T* Singleton<T>::m_pInstance = NULL;

由於new運算符的調用分爲分配內存、調用構造函數以及爲指針賦值三步,就像下面的構造函數調用:」

1 SingletonInstance pInstance = new SingletonInstance();

  「這行代碼會轉化爲如下形式:」

1 SingletonInstance pHeap = __new(sizeof(SingletonInstance));
2 pHeap->SingletonInstance::SingletonInstance();
3 SingletonInstance pInstance = pHeap;

  「這樣轉換是由於在C++標準中規定,若是內存分配失敗,或者構造函數沒有成功執行, new運算符所返回的將是空。通常狀況下,編譯器不會輕易調整這三步的執行順序,可是在知足特定條件時,如構造函數不會拋出異常等,編譯器可能出於優化的目的將第一步和第三步合併爲同一步:」

1 SingletonInstance pInstance = __new(sizeof(SingletonInstance));
2 pInstance->SingletonInstance::SingletonInstance();

  「這樣就可能致使其中一個線程在完成了內存分配後就被切換到另外一線程,而另外一線程對Singleton的再次訪問將因爲pInstance已經賦值而越過if分支,從而返回一個不完整的對象。所以,我在這個實現中爲靜態成員指針添加了volatile關鍵字。該關鍵字的實際意義是由其修飾的變量可能會被意想不到地改變,所以每次對其所修飾的變量進行操做都須要從內存中取得它的實際值。它能夠用來阻止編譯器對指令順序的調整。只是因爲該關鍵字所提供的禁止重排代碼是假定在單線程環境下的,所以並不能禁止多線程環境下的指令重排。」

  「最後來講說我對atexit()關鍵字的使用。在經過new關鍵字建立類型實例的時候,咱們同時經過atexit()函數註冊了釋放該實例的函數,從而保證了這些實例可以在程序退出前正確地析構。該函數的特性也能保證後被建立的實例首先被析構。其實,對靜態類型實例進行析構的過程與前面所提到的在main()函數執行以前插入靜態初始化邏輯相對應。」

 

引用仍是指針

  「既然你在實現中使用了指針,爲何仍然在Instance()函數中返回引用呢?」面試官又拋出了新的問題。

  「這是由於Singleton返回的實例的生存期是由Singleton自己所決定的,而不是用戶代碼。咱們知道,指針和引用在語法上的最大區別就是指針能夠爲NULL,並能夠經過delete運算符刪除指針所指的實例,而引用則不能夠。由該語法區別引伸出的語義區別之一就是這些實例的生存期意義:經過引用所返回的實例,生存期由非用戶代碼管理,而經過指針返回的實例,其可能在某個時間點沒有被建立,或是能夠被刪除的。可是這兩條Singleton都不知足,所以在這裏,我使用指針,而不是引用。」

  「指針和引用除了你提到的這些以外,還有其它的區別嗎?」

  「有的。指針和引用的區別主要存在於幾個方面。從低層次向高層次上來講,分爲編譯器實現上的,語法上的以及語義上的區別。就編譯器的實現來講,聲明一個引用並無爲引用分配內存,而僅僅是爲該變量賦予了一個別名。而聲明一個指針則分配了內存。這種實現上的差別就致使了語法上的衆多區別:對引用進行更改將致使其本來指向的實例被賦值,而對指針進行更改將致使其指向另外一個實例;引用將永遠指向一個類型實例,從而致使其不能爲NULL,並因爲該限制而致使了衆多語法上的區別,如dynamic_cast對引用和指針在沒法成功進行轉化時的行爲不一致。而就語義而言,前面所提到的生存期語義是一個區別,同時一個返回引用的函數經常保證其返回結果有效。通常來講,語義區別的根源經常是語法上的區別,所以上面的語義區別僅僅是列舉了一些例子,而真正語義上的差異經常須要考慮它們的語境。」

  「你在前面說到了你的多線程內部實現使用了指針,而返回類型是引用。在編寫過程當中,你是否考慮了實例構造不成功的狀況,如new運算符運行失敗?」

  「是的。在和其它人進行討論的過程當中,你們對於這種問題有各自的理解。首先,對一個實例的構造將可能在兩處拋出異常:new運算符的執行以及構造函數拋出的異常。對於new運算符,我想說的是幾點。對於某些操做系統,例如Windows,其經常使用虛擬地址,所以其運行經常不受物理內存實際大小的限制。而對於構造函數中拋出的異常,咱們有兩種策略能夠選擇:在構造函數內對異常進行處理,以及在構造函數以外對異常進行處理。在構造函數內對異常進行處理能夠保證類型實例處於一個有效的狀態,但通常不是咱們想要的實例狀態。這樣一個實例會致使後面對它的使用更爲繁瑣,例如須要更多的處理邏輯或再次致使程序執行異常。反過來,在構造函數以外對異常進行處理經常是更好的選擇,由於軟件開發人員能夠根據產生異常時所構造的實例的狀態將必定範圍內的各個變量更改成合法的狀態。舉例來講,咱們在一個函數中嘗試建立一對相互關聯的類型實例,那麼在一個實例的構造函數拋出了異常時,咱們不該該在構造函數裏對該實例的狀態進行維護,由於前一個實例的構造是按照後一個實例會正常建立來進行的。相對來講,放棄後一個實例,並將前一個實例刪除是一個比較好的選擇。」

  我在白板上比劃了一下,繼續說到:「咱們知道,異常有兩個很是明顯的缺陷:效率,以及對代碼的污染。在過小的粒度中使用異常,就會致使異常使用次數的增長,對於效率以及代碼的整潔型都是傷害。一樣地,對拷貝構造函數等組成經常須要使用相似的原則。」

  「反過來講,Singleton的使用也能夠保持着這種原則。Singleton僅僅是一個包裝好的全局實例,對其的建立若是一旦不成功,在較高層次上保持正常狀態一樣是一個較好的選擇。」

 

Anti-Patten

  「既然你提到了Singleton僅僅是一個包裝好的全局變量,那你能說說它和全局變量的相同與不一樣麼?」

  「單件能夠說是全局變量的替代品。其擁有全局變量的衆多特色:全局可見且貫穿應用程序的整個生命週期。除此以外,單件模式還擁有一些全局變量所不具備的性質:同一類型的對象實例只能有一個,同時適當的實現還擁有延遲初始化(Lazy)的功能,能夠避免耗時的全局變量初始化所致使的啓動速度不佳等問題。要說明的是,Singleton的最主要目的並非做爲一個全局變量使用,而是保證類型實例有且僅有一個。它所具備的全局訪問特性僅僅是它的一個反作用。但正是這個反作用使它更相似於包裝好的全局變量,從而容許各部分代碼對其直接進行操做。軟件開發人員須要經過仔細地閱讀各部分對其進行操做的代碼才能瞭解其真正的使用方式,而不能經過接口獲得組件依賴性等信息。若是Singleton記錄了程序的運行狀態,那麼該狀態將是一個全局狀態。各個組件對其進行操做的調用時序將變得十分重要,從而使各個組件之間存在着一種隱式的依賴。」

  「從語法上來說,首先Singleton模式實際上將類型功能與類型實例個數限制的代碼混合在了一塊兒,違反了SRP。其次Singleton模式在Instance()函數中將建立一個肯定的類型,從而禁止了經過多態提供另外一種實現的可能。」

  「可是從系統的角度來說,對Singleton的使用則是沒法避免的:假設一個系統擁有成百上千個服務,那麼對它們的傳遞將會成爲系統的一個災難。從微軟所提供的衆多類庫上來看,其經常提供一種方式得到服務的函數,如GetService()等。另一個能夠減輕Singleton模式所帶來不良影響的方法則是爲Singleton模式提供無狀態或狀態關聯很小的實現。」

  「也就是說,Singleton自己並非一個很是差的模式,對其使用的關鍵在於什麼時候使用它並正確的使用它。」

相關文章
相關標籤/搜索