程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++程序員們,快來寫最簡潔的單例模式吧,程序員例模式

C++程序員們,快來寫最簡潔的單例模式吧,程序員例模式

編輯:C++入門知識

C++程序員們,快來寫最簡潔的單例模式吧,程序員例模式


  想必每一位程序員都對設計模式中的單例模式非常的熟悉吧,以往我們用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”的。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved