c++11之單例模式
以往用C++實現一個單例模式需要寫以下程式碼:
1 class CSingleton 2 { 3 private: 4 CSingleton() //建構函式是私有的 5 { 6 } 7 static CSingleton *m_pInstance; 8 public: 9 static CSingleton * GetInstance() 10 { 11 if (m_pInstance == NULL) //判斷是否第一次呼叫 12 m_pInstance = new CSingleton();13 return m_pInstance; 14 } 15 };
當然,這份程式碼在單執行緒環境下是正確無誤的,但是當拿到多執行緒環境下時這份程式碼就會出現race condition,因此為了能在多執行緒環境下實現單例模式,我們首先想到的是利用同步機制來正確的保護我們的shared data,於是在多執行緒環境下單例模式程式碼就變成了下面這樣:
1 class CSingleton 2 { 3 private: 4 CSingleton() //建構函式是私有的 5 { 6 } 7 static CSingleton *m_pInstance;8 mutex mtx; 9 public: 10 static CSingleton * GetInstance() 11 { 12 mtx.lock(); 13 if (m_pInstance == NULL) //判斷是否第一次呼叫 14 m_pInstance = new CSingleton(); 15 mtx.unlock(); 16 return m_pInstance; 17 } 18 };
正確是正確了,問題是每次呼叫GetInstance函式都要進入臨界區,尤其是在heavy contention情況下函式將會成為系統的效能瓶頸,我們偉大的程式設計師發現我們不必每次呼叫GetInstance函式時都去獲取鎖,只是在第一次new這個例項的時候才需要同步,所以偉大的程式設計師們發明了著名的DCL技法,即Double Check Lock,程式碼如下:
1 Widget* Widget::pInstance{ nullptr }; 2 Widget* Widget::Instance() { 3 if (pInstance == nullptr) { // 1: first check 4 lock_guard<mutex> lock{ mutW }; 5 if (pInstance == nullptr) { // 2: second check 6 pInstance = new Widget(); 7 } 8 } 9 return pInstance; 10 }
曾今有一段時間,這段程式碼是被認為正確無誤的,但是一群偉大的程式設計師們發現了其中的bug!並且聯名上書表示這份程式碼是錯誤的。要解釋其中為什麼出現了錯誤,需要讀者十分的熟悉memory model,這裡我就不詳細的說明了,一句話就是在這份程式碼中第三行程式碼:if (pInstance == nullptr)和第六行程式碼pInstance = new Widget();沒有正確的同步,在某種情況下會出現new返回了地址賦值給pInstance變數而Widget此時還沒有構造完全,當另一個執行緒隨後執行到第三行時將不會進入if從而返回了不完全的例項物件給使用者使用,造成了嚴重的錯誤。在C++11沒有出來的時候,只能靠插入兩個memory barrier來解決這個錯誤,但是C++11已經出現了好幾年了,其中我認為最重要的是引進了memory model,從此C++11也能識別執行緒這個概念了!
因此,在有了C++11後我們就可以正確的跨平臺的實現DCL模式了,程式碼如下:
1 atomic<Widget*> Widget::pInstance{ nullptr }; 2 Widget* Widget::Instance() { 3 if (pInstance == nullptr) { 4 lock_guard<mutex> lock{ mutW }; 5 if (pInstance == nullptr) { 6 pInstance = new Widget(); 7 } 8 } 9 return pInstance; 10 }
C++11中的atomic類的預設memory_order_seq_cst保證了3、6行程式碼的正確同步,由於上面的atomic需要一些效能上的損失,因此我們可以寫一個優化的版本:
1 atomic<Widget*> Widget::pInstance{ nullptr }; 2 Widget* Widget::Instance() { 3 Widget* p = pInstance; 4 if (p == nullptr) { 5 lock_guard<mutex> lock{ mutW }; 6 if ((p = pInstance) == nullptr) { 7 pInstance = p = new Widget(); 8 } 9 } 10 return p; 11 }
但是,C++委員會考慮到單例模式的廣泛應用,所以提供了一個更加方便的元件來完成相同的功能:
1 static unique_ptr<widget> widget::instance; 2 static std::once_flag widget::create; 3 widget& widget::get_instance() { 4 std::call_once(create, [=]{ instance = make_unique<widget>(); }); 5 return instance; 6 }
可以看出上面的程式碼相比較之前的示例程式碼來說已經相當的簡潔了,但是!!!有是但是!!!!在C++memory model中對static local variable,說道:The initialization of such a variable is defined to occur the first time control passes through its declaration; for multiple threads calling the function, this means there’s the potential for a race condition to define first.因此,我們將會得到一份最簡潔也是效率最高的單例模式的C++11實現:
1 widget& widget::get_instance() { 2 static widget instance; 3 return instance; 4 }
用Herb Sutter的話來說這份程式碼實現是“Best of All”的。