日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
C++11中的雙重檢查鎖定模式

雙重檢查鎖定模式(DCLP)在無鎖編程方面是有點(diǎn)兒臭名昭著案例學(xué)術(shù)研究的味道。直到2004年,使用java開發(fā)并沒有安全的方式來實現(xiàn)它。在 c++11之前,使用便捷式c+開發(fā)并沒有安全的方式來實現(xiàn)它。由于引起人們關(guān)注的缺點(diǎn)模式暴露在這些語言之中,人們開始寫它。一組高調(diào)的java聚集在 一起開發(fā)人員并簽署了一項聲明,題為:“雙重檢查鎖定壞了”。在2004年斯科特 、梅爾斯和安德烈、亞歷山發(fā)表了一篇文章,題為:“c+與雙重檢查鎖定 的危險”對于DCLP是什么?這兩篇文章都是偉大的引物,為什么呢?在當(dāng)時看來,這些語言都不足以實現(xiàn)它。

創(chuàng)新互聯(lián)主營云溪網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營網(wǎng)站建設(shè)方案,重慶APP開發(fā),云溪h5微信小程序開發(fā)搭建,云溪網(wǎng)站營銷推廣歡迎云溪等地區(qū)企業(yè)咨詢

在過去。java現(xiàn)在可以為修訂內(nèi)存模型,為thevolatileeyword注入新的語義,使得它盡可然安全實現(xiàn)DCLP.同樣地,c+11有一個全 新的內(nèi)存模型和原子庫使得各種各樣的便捷式DCLP得以實現(xiàn)。c+11反過來啟發(fā)Mintomic,一個小型圖書館,我今年早些時候發(fā)布的,這使得它盡可 能的實現(xiàn)一些較舊的c/c++編譯器以及DCLP.

在這篇文章中,我將重點(diǎn)關(guān)注c++實現(xiàn)的DCLP.

什么是雙重檢查鎖定?

假設(shè)你有一個類,它實現(xiàn)了著名的Singleton 模式,現(xiàn)在你想讓它變得線程安全。顯然的一個方法就是通過增加一個鎖來保證互斥共享。這樣的話,如果有兩個線程同時調(diào)用了Singleton::getInstance,將只有其中之一會創(chuàng)建這個單例。

 
 
 
 
  1. Singleton* Singleton::getInstance() { Lock lock; // scope-based lock, released automatically when the function returns if (m_instance == NULL) {
  2.         m_instance = new Singleton;
  3.     } return m_instance;
  4. }

這是完全合法的方法,但是一旦單例被創(chuàng)建,實際上就不再需要鎖了。鎖不一定慢,但是在高并發(fā)的條件下,不具有很好的伸縮性。

雙重檢查鎖定模式避免了在單例已經(jīng)存在時候的鎖定。不過如Meyers-Alexandrescu的論文所顯示的,它并不簡單。在那篇論文中,作者描述了幾個有缺陷的用C++實現(xiàn)DCLP的嘗試,并剖析了每種情況為什么是不安全的。最后,在第12頁,他們給出了一個安全的實現(xiàn),但是它依賴于非指定的,特定平臺的內(nèi)存屏障(memory barriers)。

(譯注:內(nèi)存屏障就是一種干預(yù)手段. 他們能保證處于內(nèi)存屏障兩邊的內(nèi)存操作滿足部分有序)

 
 
 
 
  1. Singleton* Singleton::getInstance() { 
  2.     Singleton* tmp = m_instance; ... // insert memory barrier if (tmp == NULL) { 
  3.         Lock lock; 
  4.         tmp = m_instance; if (tmp == NULL) { 
  5.             tmp = new Singleton; ... // insert memory barrier m_instance = tmp; 
  6.         } 
  7.     } return tmp; 

這里,我們可以發(fā)現(xiàn)雙重檢查鎖定模式是由此得名的:在單例指針m_instance為NULL的時候,我們僅僅使用了一個鎖,這個鎖使偶然訪問到該單例的第一組線程繼續(xù)下去。而在鎖的內(nèi)部,m_instance被再次檢查,這樣就只有第一個線程可以創(chuàng)建這個單例了。

這與可運(yùn)行的實現(xiàn)非常相近。只是在突出顯示的幾行漏掉了某種內(nèi)存屏障。在作者寫這篇論文的時候,還沒有填補(bǔ)此項空白的輕便的C/C++函數(shù)?,F(xiàn)在,C++11已經(jīng)有了。

用 C++11 獲得與釋放屏障

你可以用獲得與釋放屏障 安全的完成上述實現(xiàn),在我以前的文章中我已經(jīng)詳細(xì)的解釋過這個主題。不過,為了讓代碼真正的具有可移植性,你還必須要將m_instance包裝成原子類型,并且用放松的原子操作(譯注:即非原子操作)來操作它。這里給出的是結(jié)果代碼,獲取與釋放屏障部分高亮了。

 
 
 
 
  1. std::atomic Singleton::m_instance;
  2. std::mutex Singleton::m_mutex;
  3. Singleton* Singleton::getInstance() {
  4.     Singleton* tmp = m_instance.load(std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_acquire); if (tmp == nullptr) {
  5.         std::lock_guard lock(m_mutex);
  6.         tmp = m_instance.load(std::memory_order_relaxed); if (tmp == nullptr) {
  7.             tmp = new Singleton; std::atomic_thread_fence(std::memory_order_release); m_instance.store(tmp, std::memory_order_relaxed);
  8.         }
  9.     } return tmp;
  10. }

即使是在多核系統(tǒng)上,它也可以令人信賴的工作,因為內(nèi)存屏障在創(chuàng)建單例的線程與其后任何跳過這個鎖的線程之間,創(chuàng)建了一種同步的關(guān)系。Singleton::m_instance充當(dāng)警衛(wèi)變量,而單例本身的內(nèi)容充當(dāng)有效載荷。

所有那些有缺陷的DCLP實現(xiàn)都忽視了這一點(diǎn):如果沒有同步的關(guān)系,將無法保證第一個線程的所有寫操作——特別是,那些在單例構(gòu)造器中執(zhí)行的寫操作——可以對第二個線程可見,雖然m_instance指針本身是可見的!第一個線程具有的鎖也對此無能為力,因為第二個線程不必獲得任何鎖,因此它能并發(fā)的運(yùn)行。

如果你想更深入的理解這些屏障為什么以及如何使得DCLP具有可信賴性,在我以前的文章中有一些背景信息,就像這個博客早前的文章一樣。

使用 Mintomic 屏障

Mintomic 是一個小型的C語言的庫,它提供了C++11原子庫的一個功能子集,其中包含有獲取與釋放屏障,而且它是運(yùn)行于更老的編譯器之上的。Mintomic依賴于這樣的假設(shè) ,即C++11的內(nèi)存模型——特殊的是,其中包括無中生有的存儲 ——因為它不被更老的編譯器支持,不過這已經(jīng)是我們不通過C++11能做到的最佳程度了。記住這些東西可是若干年來我們在寫多線程C++代碼時的環(huán)境。無 中生有的存儲(Out-of-thin-air stores)已被時間證明是不流行的,而且好的編譯器也基本上不會這么做。

這里有一個DCLP的實現(xiàn),就是用Mintomic來獲取與釋放屏障的。和前面使用C++11獲取和釋放屏障的例子比起來,它基本上是等效的。

 
 
 
 
  1. mint_atomicPtr_t Singleton::m_instance = { 0 };
  2. mint_mutex_t Singleton::m_mutex;
  3. Singleton* Singleton::getInstance() {
  4.     Singleton* tmp = (Singleton*) mint_load_ptr_relaxed(&m_instance); mint_thread_fence_acquire(); if (tmp == NULL) {
  5.         mint_mutex_lock(&m_mutex);
  6.         tmp = (Singleton*) mint_load_ptr_relaxed(&m_instance); if (tmp == NULL) {
  7.             tmp = new Singleton; mint_thread_fence_release(); mint_store_ptr_relaxed(&m_instance, tmp);
  8.         }
  9.         mint_mutex_unlock(&m_mutex);
  10.     } return tmp;
  11. }

#p#

使用c++ 11低級排序約束

C++11的獲取與釋放屏障可以正確的實現(xiàn)DCLP,而且應(yīng)該能夠針對當(dāng)今大多數(shù)的多核設(shè)備,生成優(yōu)化的機(jī)器代碼(就像Mintomic做的那樣),但是它們似乎不是非常時髦。在C++11中獲得同等效果的選擇方法,應(yīng)該是使用基于低級排序約束的原子操作。正如我先前所說,一條寫釋放(write-release)可以同步于一條讀獲取(read-acquire)。

 
 
 
 
  1. std::atomic Singleton::m_instance;
  2. std::mutex Singleton::m_mutex;
  3. Singleton* Singleton::getInstance() { Singleton* tmp = m_instance.load(std::memory_order_acquire); if (tmp == nullptr) {
  4.         std::lock_guard lock(m_mutex);
  5.         tmp = m_instance.load(std::memory_order_relaxed); if (tmp == nullptr) {
  6.             tmp = new Singleton; m_instance.store(tmp, std::memory_order_release); }
  7.     } return tmp;
  8. }

從技術(shù)上說,這種無鎖的同步形式,比使用獨(dú)立屏障的形式,要不那么嚴(yán)格;上面的操作只是意味著阻止它們自己周圍的內(nèi)存重新排序,這與獨(dú)立的屏障不同,后者意味著阻止所有相鄰的操作的特定類型的內(nèi)存重排序。盡管如此,在x86/64, ARMv6/v7,以及 PowerPC架構(gòu)上,對于這兩種形式,可能的最好代碼都是相同的。例如,在一篇早前文章中,我演示了在ARMv7編譯器上,C++11低級排序約束是如何發(fā)送dmb指令的,而這也正是你在使用獨(dú)立屏障時所期待的同樣事情。

這兩種形式有可能會生成不同機(jī)器代碼的一個平臺是Itanium。Itanium可以使用一條單獨(dú)的CPU指令,ld.acq來實現(xiàn)C++11的 load(memory_order_acquire),并可以使用st.rel來實現(xiàn)store(tmp, memory_order_release)。我很想研究一下這些指令與獨(dú)立屏障之間的性能差異,可是我找不到可用的Itanium機(jī)器。

另一個這樣的平臺是最近出現(xiàn)的ARMv8架構(gòu)。ARMv8提供了ldar和stlr指令,除了它們也增強(qiáng)了stlr指令以及任何后續(xù)的ldar指令之間的存儲加載排序以外,其它的都與Itanium的ld.acq和st.rel指令很相似。事實上,ARMv8的這些新指令意在實現(xiàn)C++11的SC原子操作,這在后面會講到。

使用 C++11的順序一致原子

C++11提供了一種完全不同的方法來寫無鎖代碼。(我們可以認(rèn)為在某些特定的代碼路徑上DCLP是“無鎖”的,因為并不是所有的線程都具有鎖。)如果在 所有原子庫函數(shù)上,你忽略了可選的std::memory_order參數(shù),那么默認(rèn)值std::memory_order_seq_cst就會將所有的 原子變量轉(zhuǎn)變?yōu)轫樞蛞恢碌?sequentially consistent) (SC)原子。通過SC原子,只要不存在數(shù)據(jù)競爭,整個算法就可以保證是順序一致的。SC原子Java 5+中的volatile變量非常相似。

這里是使用SC原子的一個DCLP實現(xiàn)。如之前所有例子一樣,一旦單例被創(chuàng)建,第二行高亮將與第一行同步。

 
 
 
 
  1. std::atomic Singleton::m_instance;
  2. std::mutex Singleton::m_mutex;
  3. Singleton* Singleton::getInstance() { Singleton* tmp = m_instance.load(); if (tmp == nullptr) {
  4.         std::lock_guard lock(m_mutex);
  5.         tmp = m_instance.load(); if (tmp == nullptr) {
  6.             tmp = new Singleton; m_instance.store(tmp); }
  7.     } return tmp;
  8. }

SC原子被認(rèn)為可以使程序員更容易思考。其代價是生成的機(jī)器代碼似乎比之前的例子效率要低。例如,這里有有一些關(guān)于上面代碼清單的x64機(jī)器代碼,由Clang 3.3在啟用代碼優(yōu)化的條件下生成:

由于我們使用了SC原子,保存到m_instance是由xchg指令實現(xiàn)的,在x64上它具有內(nèi)存屏障作用。這比x64中DCLP實際需要的指令更強(qiáng)。 只需一條簡單的mov指令就可以做這項工作。不過這并不十分要緊,因為在單例首次創(chuàng)建的代碼路徑上,xchg指令只下發(fā)一次。

另一方面,如果你給PowerPC 或 ARMv6/v7編譯SC原子指令,你十有八九會得到糟糕的機(jī)器代碼。其中的細(xì)節(jié),請看Herb Sutter的atomic<> 武器說話,第 2部分的00:44:25 - 00:49:16段落。

使用 C++11 的數(shù)據(jù)相關(guān)性排序

在上面所有我給出的例子中,在創(chuàng)建單例的那個線程,與其后任何越過鎖的線程之間,有一種同步的關(guān)系。警衛(wèi)變量就是單例指針,有效載荷是單例自身的內(nèi)容。在本例中,有效載荷被認(rèn)為是警衛(wèi)指針的一個相關(guān)性數(shù)據(jù)。

人們后來發(fā)現(xiàn),當(dāng)存在數(shù)據(jù)相關(guān)性時,上面所有例子中都用到的讀獲?。╮ead-acquire)操作,將極富殺傷力!我們用消費(fèi)操作(consume operation)來替代它要好一點(diǎn)。消費(fèi)操作很酷,因為它們消除了PowerPC中的一條lwsync指令,以及ARMv7中的一條dmb指令。在將來的一篇文章中,我將更多的談?wù)摰接嘘P(guān)數(shù)據(jù)相關(guān)性和消費(fèi)操作的內(nèi)容。

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

有些讀者已經(jīng)知道這篇文章的妙語:如果你想得到一個線程安全的實例,C++11不允許你跳過以上的所有步驟。你可以簡單使用一個靜態(tài)初始化器。

 
 
 
 
  1. Singleton& Singleton::getInstance() { 
  2.     static Singleton instance; return instance;
  3. }

讓我們回到6.7.6節(jié)查看C++11的標(biāo)準(zhǔn):

  如果控制進(jìn)入申明同時變量將被初始化的時候,那么并發(fā)執(zhí)行將會等到初始化的完成。

由編譯器來臨時代替實現(xiàn)的細(xì)節(jié),DCLP明顯是一個不錯的選擇。不能保證編譯器將會使用DCLP,但一些(也許更多)卻碰巧發(fā)生了。使用the-std=c++0x選項對ARM進(jìn)行編譯,生成了下面的一些機(jī)器碼,這些機(jī)器碼是由GCC 4.6生成的。

由于單例創(chuàng)建于固定地址,為了同步的目的,編譯器引進(jìn)了一個獨(dú)立的警衛(wèi)變量。特別需要注意的 是,在最初讀到這個警衛(wèi)變量之后,并沒有現(xiàn)成的dmb指令可以用來獲取內(nèi)存屏障。警衛(wèi)變量是指向單例的指針,因此編譯器可以利用數(shù)據(jù)相關(guān)性,省略掉這種 dmb指令。__cxa_guard_release對警衛(wèi)變量執(zhí)行了一個寫釋放(write-release)操作,這樣只要警衛(wèi)變量已設(shè)置,在讀消費(fèi) (read-consume)之前就建立了依賴順序,就像前面所有例子里那樣,基于對內(nèi)存的重新排序,整個事情開始變得有彈性。

如你所見,我們已伴隨C++11走過了一段漫長的道路。雙重檢查鎖定是一種穩(wěn)定的模式,而且還遠(yuǎn)不止此!

就個人而言,我常常想,如果是需要初始化一個單例,最好是在程序啟動的時候做這個事情。但是顯然DCLP可以拯救你于泥潭。而且在實際的使用中,你還可以用DCLP來將任意數(shù)值類型存儲到一個無鎖的哈希表。在以后的文章中會有更多關(guān)于它的論述。


文章題目:C++11中的雙重檢查鎖定模式
網(wǎng)站網(wǎng)址:http://www.dlmjj.cn/article/coehhoc.html