天天看點

單例 ------ C++實作

基礎知識掌握:

單例考慮三點:記憶體何時釋放、運作速度如何、多線程下能否保證隻有一個執行個體

如果擷取對象的傳回值類型是引用,傳回值指派給變量而不是引用會進行對象的拷貝,這樣就會出現兩個對象,可以把顯示聲明拷貝構造函數(包括 = 操作符)為private,這樣就不會進行對象的拷貝

如果擷取對象的傳回值是指針,一方面需要建立一個指針接收傳回的對象指針,另一方面可能誤執行delete把這個對象銷毀了,此對象的銷毀推薦設計在程式結束後自行銷毀。

程式在結束時會自動回收(析構)全局作用範圍内的變量(全局變量和靜态變量),但是new出來的不由程式回收,可能通過系統回收,系統也可能不回收。

靜态變量的記憶體配置設定和初始化

全局變量、non-local static變量(檔案域的靜态變量和類的靜态成員變量)在main執行之前的靜态初始化過程中配置設定記憶體并初始化;local static 變量(局部靜态變量)則是編譯時配置設定記憶體,第一次使用時初始化。這裡的變量包含内置資料類型和自定義類型的對象。

靜态變量初始化的線程安全性說明

1、非局部靜态變量一般在main執行之前的靜态初始化過程中配置設定記憶體并初始化,可以認為是線程安全的;

2、局部靜态變量在編譯時,編譯器的實作一般是在初始化語句之前設定一個局部靜态變量的辨別來判斷是否已經初始化,運作的時候每次進行判斷,如果需要初始化則執行初始化操作,否則不執行。這個過程本身不是線程安全的。C++0x之後該實作是線程安全的。

  C++11标準針規定了局部靜态變量初始化需要保證線程安全,C++03标準并無此說明,具體說明如下:  

    If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization 

單例運用場景:

設計模式經典GoF定義的單例模式需要滿足以下兩個條件:

1)保證一個類隻建立一個執行個體;

2)提供對該執行個體的全局通路點。

如果系統有類似的實體(有且隻有一個,且需要全局通路),那麼就可以将其實作為一個單例。實際工作中常見的應用舉例:

1)日志類,一個應用往往隻對應一個日志執行個體。

2)配置類,應用的配置集中管理,并提供全局通路。

3)管理器,比如windows系統的任務管理器就是一個例子,總是隻有一個管理器的執行個體。

4)共享資源類,加載資源需要較長時間,使用單例可以避免重複加載資源,并被多個地方共享通路。

單例實作:

Lazy Singleton(懶漢模式)

首先看GoF在描述單例模式時提出的一種實作,教科書式的例子。

這種方法的好處在于直到 Instance() 被通路,才會生成執行個體,這種特性被稱為延遲初始化(Lazy Initialization),這在一些初始化時消耗較大的情況有很大優勢。

Lazy Singleton不是線程安全的,比如現在有線程A和線程B,都通過了 ps == NULL 的判斷,那麼線程A和B都會建立新執行個體。單例模式保證生成唯一執行個體的規則被打破了。

Eager Singleton(餓漢模式)

這種實作在編譯器初始化的時候就完成了執行個體的建立,和上述的Lazy Singleton相反。

由于執行個體化是在初始化階段執行的,是以沒有線程安全的問題,但是潛在問題在于no-local static對象(函數外的static對象)在不同編譯單元(可了解為cpp檔案和其包含的頭檔案)中的初始化順序是未定義的。如果在初始化完成之前調用 Instance()方法會傳回一個未定義的執行個體。例如有兩個單例 SingletonA 和 SingletonB ,都采用了 Eager Initialization ,那麼如果 SingletonA 的初始化需要 SingletonB ,而這兩個單例又在不同的編譯單元,初始化順序是不定的,如果 SingletonA 在 SingletonB 之前初始化,就會出錯,以下舉例說明:

執行個體:ASingleton、BSingleton兩個單例類,其中 ASingleton 的構造函數中使用到 BSingleton 的單例對象。

在這個測試例子中,我們将上述代碼放在一個 main.cpp 檔案中,其中 main 函數為空。

運作測試結果是:

BSingleton 的構造函數居然是在成員函數 do_something 之後調用的!

Meyers Singleton

為了解決上面的問題,Scott Meyers在《Effective C++》(Item 04)中的提出另一種更優雅的單例模式實作,使用local static對象(函數内的static對象)。當第一次通路 Instance() 方法時才建立執行個體。

C++0x之後該實作是線程安全的,有興趣可以讀相關的标準草案(section 6.7),編譯器的支援程度不一定,但是G++4.0及以上是支援的。

這裡推薦一個餓漢式單例,不要求局部靜态變量線程安全,并且避免了初始化順序問題。boost 的實作方式是:單例對象作為靜态局部變量,然後增加一個輔助類,并聲明一個該輔助類的類靜态成員變量,在該輔助類的構造函數中,初始化單例對象。

Double-Checked Locking Pattern(雙檢測鎖模式)

回顧 Lazy Singleton 模式,考慮到線程安全,我們可以通過加鎖來保護單例初始化這一過程,雙檢測鎖模式就是在懶漢模式的基礎上稍作修改得到:

以上的上鎖和解鎖僅用于說明,實際應用中可以使用互斥鎖,單一信号量等方法去實作。

這裡的兩次 ps == NULL,是借鑒了Java的單例模式實作時,使用的所謂的“雙檢鎖模式”(Double-Checked Locking Pattern)。因為進行一次加鎖和解鎖是需要付出對應的代價的,而進行兩次判斷,就可以避免多次加鎖與解鎖操作,同時也保證了線程安全。理論上問題解決了,但是在實踐中有很多坑,如指令重排、多核處理器等問題讓DCLP實作起來比較複雜比如需要使用記憶體屏障,詳細的分析可以閱讀這篇論文《C++ and the Perils of Double-Checked Locking》。

在C++11中有全新的記憶體模型和原子庫,可以很友善的用來實作DCLP。這裡不展開。有興趣可以閱讀這篇文章《Double-Checked Locking is Fixed In C++11》。

pthread_once

在多線程程式設計環境下,盡管 pthread_once() 調用會出現在多個線程中,init_routine()函數僅執行一次,pthread_once是很适合用來實作線程安全單例。(pthread_once 在一個程序裡隻會執行一次,其實作方式使用的就是互斥鎖+條件變量的方法)

這裡的boost::noncopyable的作用是把構造函數, 指派函數, 析構函數, 複制構造函數聲明為私有或者保護。

不使用模版,如下:

下一篇: 好文章收藏

繼續閱讀