溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

C++11中的雙重檢查鎖定是什么意思

發(fā)布時間:2021-09-09 09:13:38 來源:億速云 閱讀:197 作者:chen 欄目:編程語言

本篇內(nèi)容介紹了“C++11中的雙重檢查鎖定是什么意思”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠?qū)W有所成!

什么是雙重檢查鎖定?

如果你想在多線程編程中安全使用單件模式(Singleton),最簡單的做法是在訪問時對其加鎖,使用這種方式,假定兩個線程同時調(diào)用Singleton::getInstance方法,其中之一負責創(chuàng)建單件:

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; }

使用這種方式是可行的,但是當單件被創(chuàng)建之后,實際上你已經(jīng)不需要再對其進行加鎖,加鎖雖然不一定導致性能低下,但是在重負載情況下,這也可能導致響應緩慢。

使用雙重檢查鎖定模式避免了在單件對象已經(jīng)創(chuàng)建好之后進行不必要的鎖定,然而實現(xiàn)卻有點復雜,在Meyers-Alexandrescu的論文中也 有過闡述,文中提出了幾種存在缺陷的實現(xiàn)方式,并逐一解釋了為什么這樣實現(xiàn)存在問題。在論文的結(jié)尾的第12頁,給出了一種可靠的實現(xiàn)方式,實現(xiàn)依賴一種標 準中未規(guī)范的內(nèi)存柵欄技術(shù)。

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; }

這里,我們可以看到:如模式名稱一樣,代碼中實現(xiàn)了雙重校驗,在m_instance指針為NULL時,我們做了一次鎖定,這一過程在***創(chuàng)建該對象的線程可見。在創(chuàng)建線程內(nèi)部構(gòu)造塊中,m_instance被再一次檢查,以確保該線程僅創(chuàng)建了一份對象副本。

這是雙重檢查鎖定的實現(xiàn),只不過在被高亮的代碼行中還缺乏了內(nèi)存柵欄技術(shù)做保證,在此文寫就之際,C/C++各編譯器未對該實現(xiàn)進行統(tǒng)一,而在C++11標準中,對這種情況下的實現(xiàn)進行了完善和統(tǒng)一。

在C++11中獲取和釋放內(nèi)存柵欄

在C++11中,你可以獲取和釋放內(nèi)存柵欄來實現(xiàn)上述功能(如何獲取和釋放內(nèi)存柵欄在我上一篇博文中有講述)。為了使你的代碼在C++各種實現(xiàn)中具 備更好的可移植性,你應該使用C++11中新增的atomic類型來包裝你的m_instance指針,這使得對m_instance的操作是一個原子操作。下面的代碼演示了如何使用內(nèi)存柵欄,請注意代碼高亮部分:

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; }

上述代碼在多核系統(tǒng)中仍然工作正常,這是因為內(nèi)存柵欄技術(shù)在創(chuàng)建對象線程和使用對象線程之間建立了一種“同步-與”的關(guān)系(synchronizes-with)。Singleton::m_instance扮演了守衛(wèi)變量的角色,而單件本身則作為負載內(nèi)容。

C++11中的雙重檢查鎖定是什么意思

而其他存在缺陷的雙重檢查鎖定實現(xiàn)都缺乏該機制的保障:在沒有“同步-與”關(guān)系保證的情況下,***個創(chuàng)建線程的寫操作,確切地說是在其構(gòu)造函數(shù)中, 可以被其他線程感知,即m_instance指針能被其他線程訪問!創(chuàng)建單件線程中的鎖也不起作用,由于該鎖對其他線程不可見,從而導致在某些情況下,創(chuàng) 建對象被執(zhí)行多次。

如果你想了解關(guān)于內(nèi)存柵欄技術(shù)是如何可靠實現(xiàn)雙重檢查鎖定的內(nèi)部原理,在我的前一篇文章中有一些背景信息(previous post),之前的博客也有一些相關(guān)內(nèi)容。

使用Mintomic 內(nèi)存柵欄

Mintomic是一個很小的c庫,提供了C++11  atomic庫中的一些功能函數(shù)子集,包含獲取和釋放內(nèi)存柵欄,同時它能工作在早期的編譯器之上。Mintomic依賴于與C++11相似的內(nèi)存模型&mdash;&mdash; 確切地說是不使用Out-of-thin-air存儲&mdash;&mdash;這一技術(shù)在早期編譯器中未進行實現(xiàn),而這是在沒有C++11標準情況下我們能做的***實現(xiàn)。以我 多年C++多線程開發(fā)的經(jīng)驗看來,Out-of-thin-air存儲并不流行,而且大多數(shù)編譯器會避免實現(xiàn)它。

下面的代碼演示了如何使用Mintomic的獲取和釋放內(nèi)存柵欄機制實現(xiàn)雙重檢查鎖定,基本上與上面的例子類似:

mint_atomicPtr_t Singleton::m_instance = { 0 }; mint_mutex_t Singleton::m_mutex;   Singleton* Singleton::getInstance() {     Singleton* tmp = (Singleton*) mint_load_ptr_relaxed(&m_instance);     mint_thread_fence_acquire();     if (tmp == NULL) {         mint_mutex_lock(&m_mutex);         tmp = (Singleton*) mint_load_ptr_relaxed(&m_instance);         if (tmp == NULL) {             tmp = new Singleton;             mint_thread_fence_release();             mint_store_ptr_relaxed(&m_instance, tmp);         }         mint_mutex_unlock(&m_mutex);     }     return tmp; }

為了實現(xiàn)獲取和釋放內(nèi)存柵欄,Mintomic會試圖在其支持的編譯器平臺產(chǎn)生***效的機器碼。例如,下面的匯編代碼來自Xbox 360,使用的是PowerPC處理器。在該平臺上,內(nèi)聯(lián)的lwsync關(guān)鍵字是針對獲取和釋放內(nèi)存柵欄的優(yōu)化指令。

C++11中的雙重檢查鎖定是什么意思

上述采用C++11標準庫編譯的例子在PowerPC處理器編譯應該會產(chǎn)生一樣的匯編代碼(理想情況下)。不過,我沒有能夠在PowerPC下編譯C++11來驗證這一點。

使用C++11低階指令順序約束

在C++11中使用內(nèi)存柵欄鎖定技術(shù)可以很方便地實現(xiàn)雙重檢查鎖定。同時也保證在現(xiàn)今流行的多核系統(tǒng)中產(chǎn)生優(yōu)化的機器碼(Mintomic也能做到 這一點)。不過使用這種方式并不是常用,在C++11中更好的實現(xiàn)方式是使用保證低階指令執(zhí)行順序約束的原子操作。之前的圖片中可以看到,一個寫-釋放操 作可以與一個獲取-讀操作同步:

std::atomic<Singleton*> Singleton::m_instance; std::mutex Singleton::m_mutex;   Singleton* Singleton::getInstance() {     Singleton* tmp = m_instance.load(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;             m_instance.store(tmp, std::memory_order_release);         }     }     return tmp; }

從技術(shù)上講,使用這種形式的無鎖同步比獨立內(nèi)存柵欄技術(shù)限制更低。上述操作只是為了防止自身操作的內(nèi)存排序,而內(nèi)存柵欄技術(shù)則阻止了臨近操作的內(nèi)存 排序。盡管如此,現(xiàn)今的x86/64,ARMv6 /  v7,和PowerPC處理器架構(gòu),針對這兩種形式產(chǎn)生的機器碼應該是一致的。在我之前的博文中,我展示了C++11低階指令順序約束在ARM7中使用了 dmb指令,這和使用內(nèi)存柵欄技術(shù)產(chǎn)生的匯編代碼相一致。

上述兩種方式在Itanium平臺可能產(chǎn)生不一樣的機器碼,在Itanium平臺上,C++11標準中的 load(memory_order_acquire)可以用單CPU指令:ld.acq,而store(tmp,  memory_order_release)使用st.rel就可以實現(xiàn)。

在ARMv8處理器架構(gòu)中,也提供了和Itanium指令等價的ldar 和 stlr  指令,而不同的地方是:這些指令還會導致stlr和后續(xù)ldar之間進一級的存儲裝載指令進行排序。實際上,ARMv8的新指令試圖實現(xiàn)C++11標準中 的順序約束原子操作,這會在后面進一步講述。

使用C++順序一致的原子操作

C++11標準提供了一個不同的方式來編寫無鎖程序(可以把雙重檢查鎖定歸類為無鎖編程的一種,因為不是所有線程都會獲取鎖)。在所有原子操作庫方 法中使用可選參數(shù)std::memory_order可以使得所有原子變量變?yōu)轫樞虻脑硬僮鳎╯equentially  consistent),方法的默認參數(shù)為std::memory_order_seq_cst。使用順序約束(SC)原子操作庫,整個函數(shù)執(zhí)行都將保證 順序執(zhí)行,并且不會出現(xiàn)數(shù)據(jù)競態(tài)(data races)。順序約束(SC)原子操作和JAVA5版本之后出現(xiàn)的volatile變量很相似。

使用SC原子操作實現(xiàn)雙重檢查鎖定的代碼如下:和前面的例子一樣,高亮的第二行會與***次創(chuàng)建單件的線程進行同步與操作。

std::atomic<Singleton*> Singleton::m_instance; std::mutex Singleton::m_mutex;   Singleton* Singleton::getInstance() {     Singleton* tmp = m_instance.load();     if (tmp == nullptr) {         std::lock_guard<std::mutex> lock(m_mutex);         tmp = m_instance.load();         if (tmp == nullptr) {             tmp = new Singleton;             m_instance.store(tmp);         }     }     return tmp; }

順序約束(SC)原子操作使得開發(fā)者更容易預測代碼執(zhí)行結(jié)果,不足之處在于使用順序約束(SC)原子操作類庫的代碼效率要比之前的例子低一些。例如,在x64位機器上,上述代碼使用Clang3.3優(yōu)化后產(chǎn)生如下匯編代碼:

C++11中的雙重檢查鎖定是什么意思

由于使用了順序約束(SC)原子操作類庫,變量m_instance的存儲操作使用了xchg指令,在x64處理器上相當于一個內(nèi)存柵欄操作。該指 令在x64位處理器是一個長周期指令,使用輕量級的mov指令也可以完成操作。不過,這影響不大,因為xchg指令只被單件創(chuàng)建過程調(diào)用一次。

不過,在PowerPC or ARMv6/v7處理器上編譯上述代碼,產(chǎn)生的匯編操作要糟糕得多,具體情形可以參見Herb Sutter的演講(atomic Weapons talk, part 2.00:44:25 &ndash; 00:49:16)。

使用C++11數(shù)據(jù)順序依賴原理

上面的例子都是使用了創(chuàng)建單件線程和使用單件其他線程之間的同步與關(guān)系。守衛(wèi)的是數(shù)據(jù)指針單個元素,開銷也是創(chuàng)建單件內(nèi)容本身。這里,我將演示一種使用數(shù)據(jù)依賴來保護防衛(wèi)的指針。

在使用數(shù)據(jù)依賴時候,上述例子中都使用了一個讀-獲取操作,這也會產(chǎn)生性能消耗,我們可以使用消費指令來進一步優(yōu)化。消費指令(consume  instruction)非???,在PowerPc處理器上它使用了lwsync指令,在ARMv7處理器上則編譯為dmd指令。今后我會寫一些文章來講 述消費指令和數(shù)據(jù)依賴機制。

使用C++11靜態(tài)初始化

一些讀者可能已經(jīng)知道C++11中,你可以跳過之前的檢查過程而直接得到線程安全的單件。你只需要使用一個靜態(tài)初始化:

C++11標準在6.7.4節(jié)中規(guī)定:

如果指令邏輯進入一個未被初始化的聲明變量,所有并發(fā)執(zhí)行應當?shù)却瓿稍撟兞客瓿沙跏蓟?/p>

上述操作在編譯時由編譯器保證。雙重檢查鎖定則可以利用這一點。編譯器并不保證會使用雙重檢查鎖定,但是大部分編譯器會這樣做。gcc4.6使用-std=c++0x編譯選項在ARM處理器產(chǎn)生的匯編代碼如下:

C++11中的雙重檢查鎖定是什么意思

由于單件使用的是一個固定地址,編譯器會使用一個特殊的防衛(wèi)變量來完成同步。請注意這里,在初始化變量讀操作時沒有使用dmb指令來獲取一個內(nèi)存柵 欄。守衛(wèi)變量指向了單件,因此編譯器可以使用數(shù)據(jù)依賴原則來避免使用dmb指令的開銷。__cxa_guard_release指令扮演了一個寫-釋放來 解除變量守衛(wèi)。一旦守衛(wèi)柵欄被設(shè)置,這里存在一個指令順序強制在讀-消費操作之前。這里和前面的例子一樣,對內(nèi)存排序的進行適應性的變更。

“C++11中的雙重檢查鎖定是什么意思”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

c++
AI