雙重檢查鎖定模式(DCLP)在無鎖編程(lock-free programming)中經常被討論,直到2004年,JAVA才提供了可靠的雙重檢查鎖定實現。而在C++11之前,C++沒有提供一種該模式的可移植的可靠實現。
隨著雙重檢查鎖定模式在各語言實現上存在的缺點暴露,人們開始研究如何安全可靠地實現它。2000年,一個JAVA高性能研究小組發布了一篇聲明《雙重檢查鎖定可能導致鎖定無效》。2004年,Scott Meyers 和Andrei Alexandrescu聯合發表了一篇名為《C++實現雙重檢查鎖定存在嚴重缺陷》。這兩篇論文都是重點闡述了雙重檢查鎖定DCLP)是什麼,以及雙重檢查鎖定的意義,和當前的各語言實現存在諸多不足。
現如今,JAVA為了安全地實現雙重檢查鎖定修改了其內存模型,並引入了關鍵詞volatile。與此同時,C++構建了一個全新的內存模型和原子 操作庫atomic),使得不同編譯器實現雙重檢查鎖定DCLP)更為容易。為了在更早期的C\C++編譯器中實現DCLP,在C++11引入了一個 名為Mintomic的庫,在今年早些時候由我發布了。
過去的一段時間,我都著力於C++中實現DCLP的研究。
什麼是雙重檢查鎖定?
如果你想在多線程編程中安全使用單件模式Singleton),最簡單的做法是在訪問時對其加鎖,使用這種方式,假定兩個線程同時調用Singleton::getInstance方法,其中之一負責創建單件:
- Singleton* Singleton::getInstance() {
- Lock lock; // scope-based lock, released automatically when the function returns
- if (m_instance == NULL) {
- m_instance = new Singleton;
- }
- return m_instance;
- }
使用這種方式是可行的,但是當單件被創建之後,實際上你已經不需要再對其進行加鎖,加鎖雖然不一定導致性能低下,但是在重負載情況下,這也可能導致響應緩慢。
使用雙重檢查鎖定模式避免了在單件對象已經創建好之後進行不必要的鎖定,然而實現卻有點復雜,在Meyers-Alexandrescu的論文中也 有過闡述,文中提出了幾種存在缺陷的實現方式,並逐一解釋了為什麼這樣實現存在問題。在論文的結尾的第12頁,給出了一種可靠的實現方式,實現依賴一種標 准中未規范的內存柵欄技術。
- Singleton* Singleton::getInstance() {
- Singleton* tmp = m_instance;
- ... // insert memory barrier
- if (tmp == NULL) {
- Lock lock;
- tmp = m_instance;
- if (tmp == NULL) {
- tmp = new Singleton;
- ... // insert memory barrier
- m_instance = tmp;
- }
- }
- return tmp;
- }
這裡,我們可以看到:如模式名稱一樣,代碼中實現了雙重校驗,在m_instance指針為NULL時,我們做了一次鎖定,這一過程在最先創建該對象的線程可見。在創建線程內部構造塊中,m_instance被再一次檢查,以確保該線程僅創建了一份對象副本。
這是雙重檢查鎖定的實現,只不過在被高亮的代碼行中還缺乏了內存柵欄技術做保證,在此文寫就之際,C/C++各編譯器未對該實現進行統一,而在C++11標准中,對這種情況下的實現進行了完善和統一。
在C++11中獲取和釋放內存柵欄
在C++11中,你可以獲取和釋放內存柵欄來實現上述功能如何獲取和釋放內存柵欄在我上一篇博文中有講述)。為了使你的代碼在C++各種實現中具 備更好的可移植性,你應該使用C++11中新增的atomic類型來包裝你的m_instance指針,這使得對m_instance的操作是一個原子操作。下面的代碼演示了如何使用內存柵欄,請注意代碼高亮部分:
- std::atomic<Singleton*> Singleton::m_instance;
- std::mutex Singleton::m_mutex;
- Singleton* Singleton::getInstance() {
- Singleton* tmp = m_instance.load(std::memory_order_relaxed);
- std::atomic_thread_fence(std::memory_order_acquire); // 編注:原作者提示注意的
- if (tmp == nullptr) {
- std::lock_guard<std::mutex> lock(m_mutex);
- tmp = m_instance.load(std::memory_order_relaxed);
- if (tmp == nullptr) {
- tmp = new Singleton;
- std::atomic_thread_fence(std::memory_order_release); // 編注:作者提示注意的
- m_instance.store(tmp, std::memory_order_relaxed);
- }
- }
- return tmp;
- }
上述代碼在多核系統中仍然工作正常,這是因為內存柵欄技術在創建對象線程和使用對象線程之間建立了一種“同步-與”的關系synchronizes-with)。Singleton::m_instance扮演了守衛變量的角色,而單件本身則作為負載內容。
而其他存在缺陷的雙重檢查鎖定實現都缺乏該機制的保障:在沒有“同步-與”關系保證的情況下,第一個創建線程的寫操作,確切地說是在其構造函數中, 可以被其他線程感知,即m_instance指針能被其他線程訪問!創建單件線程中的鎖也不起作用,由於該鎖對其他線程不可見,從而導致在某些情況下,創 建對象被執行多次。
如果你想了解關於內存柵欄技術是如何可靠實現雙重檢查鎖定的內部原理,在我的前一篇文章中有一些背景信息previous post),之前的博客也有一些相關內容。