前言
前段時間在網上看到了個的面試題,大概意思是如何在不使用鎖和C++11的情況下,用C++實現線程安全的Singleton。
看到這個題目后,第一個想法就是用Scott Meyer在《Effective C++》中提到的,在static成員函數中構造local static變量的方法來實現,但是經過一番查找、思考,才明白這種實現在某些情況下是有問題的。本文主要將從最基本的單線程中的Singleton開始,慢慢講述多線程與Singleton的那些事。
單線程
在單線程下,下面這個是常見的寫法:
template<typename T> class Singleton { public: static T& getInstance() { if (!value_) { value_ = new T(); } return *value_; } private: Singleton(); ~Singleton(); static T* value_; }; template<typename T> T* Singleton<T>::value_ = NULL;
在單線程中,這樣的寫法是可以正確使用的,但是在多線程中就不行了。
多線程加鎖
在多線程的環境中,上面單線程的寫法就會產生race condition從而產生多次初始化的情況。要想在多線程下工作,最容易想到的就是用鎖來保護shared variable了。下面是偽代碼:
template<typename T> class Singleton { public: static T& getInstance() { { MutexGuard guard(mutex_) // RAII if (!value_) { value_ = new T(); } } return *value_; } private: Singleton(); ~Singleton(); static T* value_; static Mutex mutex_; }; template<typename T> T* Singleton<T>::value_ = NULL; template<typename T> Mutex Singleton<T>::mutex_;
這樣在多線程下就能正常工作了。這時候,可能有人會站出來說這種做法每次調用getInstance的時候都會進入臨界區,在頻繁調用getInstance的時候會比較影響性能。這個時候,為了解決這個問題,DCL寫法就被聰明的先驅者發明了。
DCL
DCL即double-checked locking。在普通加鎖的寫法中,每次調用getInstance都會進入臨界區,這樣在heavy contention的情況下該函數就會成為系統性能的瓶頸,這個時候就有先驅者們想到了DCL寫法,也就是進行兩次check,當第一次check為假時,才加鎖進行第二次check:
template<typename T> class Singleton { public: static T& getInstance() { if(!value_) { MutexGuard guard(mutex_); if (!value_) { value_ = new T(); } } return *value_; } private: Singleton(); ~Singleton(); static T* value_; static Mutex mutex_; }; template<typename T> T* Singleton<T>::value_ = NULL; template<typename T> Mutex Singleton<T>::mutex_;
是不是覺得這樣就完美啦?其實在一段時間內,大家都以為這是正確的、有效的做法。實際上卻不是這樣的。幸運的是,后來有大牛們發現了DCL中的問題,避免了這樣錯誤的寫法在更多的程序代碼中出現。
那么到底錯在哪呢?我們先看看第12行value_ = new T這一句發生了什么:
- 分配了一個T類型對象所需要的內存。
- 在分配的內存處構造T類型的對象。
- 把分配的內存的地址賦給指針value_
主觀上,我們會覺得計算機在會按照1、2、3的步驟來執行代碼,但是問題就出在這。實際上只能確定步驟1最先執行,而步驟2、3的執行順序卻是不一定的。假如某一個線程A在調用getInstance的時候第12行的語句按照1、3、2的步驟執行,那么當剛剛執行完步驟3的時候發生線程切換,計算機開始執行另外一個線程B。因為第一次check沒有上鎖保護,那么在線程B中調用getInstance的時候,不會在第一次check上等待,而是執行這一句,那么此時value_已經被賦值了,就會直接返回*value_然后執行后面使用T類型對象的語句,但是在A線程中步驟3還沒有執行!也就是說在B線程中通過getInstance返回的對象還沒有被構造就被拿去使用了!這樣就會發生一些難以debug的災難問題。
volatile關鍵字也不會影響執行順序的不確定性。
在多核心機器的環境下,2個核心同時執行上面的A、B兩個線程時,由於第一次check沒有鎖保護,依然會出現使用實際沒有被構造的對象的情況。
關於DCL問題的詳細討論分析,可以參考Scott Meyer的paper:《C++ and the Perils of Double-Checked Locking》
不過在新的C++11中,這個問題得到了解決。因為新的C++11規定了新的內存模型,保證了執行上述3個步驟的時候不會發生線程切換,相當這個初始化過程是“原子性”的的操作,DCL又可以正確使用了,不過在C++11下卻有更簡潔的多線程Singleton寫法了,這個留在后面再介紹。
關於新的C++11的內存模型,可以參考:C++11中文版FAQ:內存模型、C++11FAQ:Memory Model、C++ Data-Dependency Ordering: Atomics and Memory Model
可能有人要問了,那么有什么辦法可以在C++11之前的版本下,使得DCL正確工作呢?要使其正確執行的話,就得在步驟2、3直接加上一道memory barrier。強迫CPU執行的時候按照1、2、3的步驟來運行。(經網友@shines77提醒,因沒有鎖的緣故這里需要用RCU技法,即read-copy-update)
static T& getInstance() { if(!value_) { MutexGuard guard(mutex_); if (!value_) { T* p = static_cast<T*>(operator new(sizeof(T))); new (p) T(); // insert some memory barier value_ = p; // RCU method } } return *value_; }
也許有人會說,你這已經把先前的value_ = new T()這一句拆成了下面這樣的兩條語句, 為什么還要在后面插入some memory barrier?
T* p = static_cast<T*>(operator new(sizeof(T))); new (p) T();
原因是現代處理器都是以Out-of-order execution(亂序執行)的方式來執行指令的。現代CPU基本都是多核心的,一個核包含多個執行單元。例如,一個現代的Intel CPU 包含6個執行單元,可以做一組數學,條件邏輯和內存操作的組合。每個執行單元可以做這些任務的組合。這些執行單元並行地操作,允許指令並行地執行。如果從其它 CPU 來觀察,這引入了程序順序的另一層不確定性。
如果站在單個CPU核心的角度上講,它(一個CPU核心)看到的程序代碼都是單線程的,所以它在內部以自己的“優化方式”亂序、並行的執行代碼,然后保證最終的結果和按代碼邏輯順序執行的結果一致。但是如果我們編寫的代碼是多線程的,當不同線程訪問、操作共享內存區域的時候,就會出現CPU實際執行的結果和代碼邏輯所期望的結果不一致的情況。這是因為以單個CPU核心的視角來看代碼是“單線程”的。
所以為了解決這個問題,就需要memory barrier了,利用它來強迫CPU按代碼的邏輯順序執行。例如上面改動版本的getInstance代碼中,因為第10行有memory barrier,所以CPU執行第9、10、11按“順序”執行的。即使在CPU核心內是並行執行指令(比如一個單元執行第9行、一個單元執行第11行)的,但是他們在退役單元(retirement unit)更新執行結果到通用寄存器或者內存中時也是按照9、10、11順序更新的。例如一個單元A先執行完了第11行,CPU讓單元A等待直到執行第9行的單元B執行完成並在退役單元更新完結果以后再在退役單元更新A的結果。
memory barreir是一種特殊的處理器指令,他指揮處理器做下面三件事:(參考文章Mutex And Memory Visibility)
- 刷新store buffer。
- 等待直到memory barreir之前的操作已經完成。
- 不將memory barreir之后的操作移到memory barreir之前執行。
通過使用memory barreir,可以確保之前的亂序執行已經全部完成,並且未完成的寫操作已全部刷新到主存。因此,數據一致性又重新回到其他線程的身邊,從而保證正確內存的可見性。實際上,原子操作以及通過原子操作實現的模型(例如一些鎖之類的),都是通過在底層加入memory barrier來實現的。
至於如何加入memory barrier,在unix上可以通過內核提供的barrier()宏來實現。或者直接嵌入ASM匯編指令mfence也可以,barrier宏也是通過該指令實現的。
關於memory barreir可以參考文章Memory Barriers/Fences。
Meyers Singleton
Scott Meyer在《Effective C++》中提出了一種簡潔的singleton寫法
template<typename T> class Singleton { public: static T& getInstance() { static T value; return value; } private: Singleton(); ~Singleton(); };
先說結論:
- 單線程下,正確。
- C++11及以后的版本(如C++14)的多線程下,正確。
- C++11之前的多線程下,不一定正確。
原因在於在C++11之前的標准中並沒有規定local static變量的內存模型,所以很多編譯器在實現local static變量的時候僅僅是進行了一次check(參考《深入探索C++對象模型》),於是getInstance函數被編譯器改寫成這樣了:
bool initialized = false; char value[sizeof(T)]; T& getInstance() { if (!initialized) { initialized = true; new (value) T(); } return *(reinterpret_cast<T*>(value)); }
於是乎它就是不是線程安全的了。
但是在C++11卻是線程安全的,這是因為新的C++標准規定了當一個線程正在初始化一個變量的時候,其他線程必須得等到該初始化完成以后才能訪問它。
在C++11 standard中的§6.7 [stmt.dcl] p4:
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
在stackoverflow中的Is Meyers implementation of Singleton pattern thread safe?這個問題中也有討論到。
不過有些編譯器在C++11之前的版本就支持這種模型,例如g++,從g++4.0開始,meyers singleton就是線程安全的,不需要C++11。其他的編譯器就需要具體的去查相關的官方手冊了。
Atomic Singleton
在C++11之前的版本下,除了通過鎖實現線程安全的Singleton外,還可以利用各個編譯器內置的atomic operation來實現。(假設類Atomic是封裝的編譯器提供的atomic operation)
template<typename T> class Singleton { public: static T& getInstance() { while (true) { if (ready_.get()) { return *value_; } else { if (initializing_.getAndSet(true)) { // another thread is initializing, waiting in circulation } else { value_ = new T(); ready_.set(true); return *value_; } } } } private: Singleton(); ~Singleton(); static Atomic<bool> ready_; static Atomic<bool> initializing_; static T* value_; }; template<typename T> Atomic<int> Singleton<T>::ready_(false); template<typename T> Atomic<int> Singleton<T>::initializing_(false); template<typename T> T* Singleton<T>::value_ = NULL;
肯定還有其他的寫法,但是思路都差不多,需要區分三種狀態:
- 對象已經構造完成
- 對象還沒有構造完成,但是某一線程正在構造中
- 對象還沒有構造完成,也沒有任何線程正在構造中
pthread_once
如果是在unix平台的話,除了使用atomic operation外,在不適用C++11的情況下,還可以通過pthread_once來實現Singleton。
pthread_once的原型為
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void))
APUE中對於pthread_once是這樣說的:
如果每個線程都調用pthread_once,系統就能保證初始化例程init_routine只被調用一次,即在系統首次調用pthread_once時。
所以,我們就可以這樣來實現Singleton了
template<typename T> class Singleton : Nocopyable { public: static T& getInstance() { threads::pthread_once(&once_control_, init); return *value_; } private: static void init() { value_ = new T(); } Singleton(); ~Singleton(); static pthread_once_t once_control_; static T* value_; }; template<typename T> pthread_once_t Singleton<T>::once_control_ = PTHREAD_ONCE_INIT; template<typename T> T* Singleton<T>::value_ = NULL;
如果需要正確的釋放資源的話,可以在init函數里面使用glibc提供的atexit函數來注冊相關的資源釋放函數,從而達到了只在進程退出時才釋放資源的這一目的。
static object
現在再回頭看看本文開頭說的面試題的要求,不用鎖和C++11,那么可以通過atomic operation來實現,但是有人會說atomic不是誇平台的,各個編譯器的實現不一樣。那么其實通過static object來實現也是可行的。
template<typename T> class Singleton { public: static T& getInstance() { return *value_; } private: Singleton(); ~Singleton(); class Helper { public: Helper() { Singleton<T>::value_ = new T(); } ~Helper() { delete value_; value_ = NULL; } }; friend class Helper; static T* value_; static Helper helper_; }; template<typename T> T* Singleton<T>::value_ = NULL; template<typename T> typename Singleton<T>::Helper Singleton<T>::helper_;
在進入main之前就把Singleton對象構造出來就可以避免在進入main函數后的多線程環境中構造的各種情況了。這種寫法有一個前提就是不能在main函數執行之前調用getInstance,因為C++標准只保證靜態變量在main函數之前之前被構造完成。
可能有人會說如果helper的初始化先於value_初始化的話,那么helper_初始化的時候就會使用尚沒有被初始化的value_,這個時候使用其返回的對象就會出現問題,或者在后面value_“真正”初始化的時候會覆蓋掉helper_初始化時賦給value_的值。
實際上這種情況不會發生,value_的初始化一定先於helper_,因為C++標准保證了這一行為:
The storage for objects with static storage duration (basic.stc.static) shall be zero-initialized (dcl.init) before any other initialization takes place. Zero-initialization and initialization with a constant expression are collectively called static initialization; all other initialization is dynamic initialization. Objects of POD types (basic.types) with static storage duration initialized with constant expressions (expr.const) shall be initialized before any dynamic initialization takes place. Objects with static storage duration defined in namespace scope in the same translation unit and dynamically initialized shall be initialized in the order in which their definition appears in the translation unit.
stackoverflow中的一個問題也討論了相關的行為,When are static C++ class members initialized?
local static
上面一種寫法只能在進入main函數后才能調用getInstance,那么有人說,我要在main函數之前調用怎么辦?
嗯,辦法還是有的。這個時候我們就可以利用local static來實現,C++標准保證函數內的local static變量在函數調用之前被初始化構造完成,利用這一特性就可以達到目的:
template<typename T> class Singleton { private: Singleton(); ~Singleton(); class Creater { public: Creater() : value_(new T()) { } ~Creater() { delete value_; value_ = NULL; } T& getValue() { return *value_; } T* value_; }; public: static T& getInstance() { static Creater creater; return creater.getValue(); } private: class Dummy { public: Dummy() { Singleton<T>::getInstance(); } }; static Dummy dummy_; }; template<typename T> typename Singleton<T>::Dummy Singleton<T>::dummy_;
這樣就可以了。dummy_的作用是即使在main函數之前沒有調用getInstance,它依然會作為最后一道屏障保證在進入main函數之前構造完成Singleton對象。這樣就避免了在進入main函數后的多線程環境中初始化的各種問題了。
但是此種方法只能在main函數執行之前的環境是單線程的環境下才能正確工作。
實際上,上文所講述了各種寫法中,有一些不能在main函數之前調用。有一些可以在main函數之前調用,但是必須在進入main之前的環境是單線程的情況下才能正常工作。具體哪種寫法是屬於這兩種情況就不一一分析了。總之,個人建議最好不要在進入main函數之前獲取Singleton對象。因為上文中的各種方法都用到了staitc member,而C++標准只保證static member在進入main函數之前初始化,但是不同編譯單元之間的static member的初始化順序卻是未定義的, 所以如果在main之前就調用getInstance的話,就有可能出現實現Singleton的static member還沒有初始化就被使用的情況。
如果萬一要在main之前獲取Singleton對象,並且進入main之前的環境是多線程環境,這種情形下,還能保證正常工作的寫法只有C++ 11下的Meyers Singleton,或者如g++ 4.0及其后續版本這樣的編譯器提前支持內存模型情況下的C++ 03也是可以的。
參考文獻
- Scott Meyers. Effective C++:55 Specific Ways to Improve Your Programs and Designs,3rd Edition. 電子工業出版社, 2011
- Stanley B. Lippman. 深度探索C++對象模型. 電子工業出版社, 2012
- Scott Meyers. C++ and the Perils of Double-Checked Locking. 2004
- 陳良喬(譯). C++11 FAQ中文版
- Bjarne Stroustrup. C++11 FAQ
- Paul E. McKenney, Hans-J. Boehm, Lawrence Crowl. C++ Data-Dependency Ordering: Atomics and Memory Model. 2008
- Wikipedia. Out-of-order execution
- Loïc. Mutex And Memory Visibility, 2009
- Randal E.Bryant, David O'Hallaron. 深入理解計算機系統(第2版). 機械工業出版社, 2010
- Martin Thompson. Memory Barriers/Fences, 2011
- Working Draft, Standard For Programing Language C++. 2012
- W.Richard Stevens. UNIX環境高級編程(第3版), 人民郵電出版社, 2014
- stackoverflow. Is Meyers implementation of Singleton pattern thread safe
- stackoverflow. When are static C++ class members initialized
(完)