自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

C++11 中的雙重檢查鎖定模式

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

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

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

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

什么是雙重檢查鎖定?

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

  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; 

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

雙重檢查鎖定模式避免了在單例已經(jīng)存在時(shí)候的鎖定。不過(guò)如Meyers-Alexandrescu的論文所顯示的,它并不簡(jiǎn)單。在那篇論文中,作者描述了幾個(gè)有缺陷的用C++實(shí)現(xiàn)DCLP的嘗試,并剖析了每種情況為什么是不安全的。最后,在第12頁(yè),他們給出了一個(gè)安全的實(shí)現(xiàn),但是它依賴于非指定的,特定平臺(tái)的內(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;  
  8. }  

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

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

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

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

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

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

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

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

使用 Mintomic 屏障

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

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

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

#p#

使用c++ 11低級(jí)排序約束

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

原文鏈接:http://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/

譯文鏈接:http://www.oschina.net/translate/double-checked-locking-is-fixed-in-cpp11

責(zé)任編輯:陳四芳 來(lái)源: 開源中國(guó)編譯
相關(guān)推薦

2013-11-29 09:51:26

C++雙重檢查鎖定

2011-04-20 10:07:15

2016-11-11 00:33:25

雙重檢查鎖定延遲初始化線程

2024-02-21 23:43:11

C++11C++開發(fā)

2013-07-31 11:09:05

C++11

2024-05-29 13:21:21

2023-09-22 22:27:54

autoC++11

2020-06-01 21:07:33

C11C++11內(nèi)存

2011-08-19 09:41:56

C++

2013-09-25 14:20:46

2020-12-09 10:55:25

ArrayvectorLinux

2013-05-30 00:49:36

C++11C++條件變量

2021-06-11 10:53:40

Folly組件開發(fā)

2023-09-24 13:58:20

C++1auto

2011-10-13 10:21:01

C++

2020-09-23 16:31:38

C++C++11啟動(dòng)線程

2012-12-25 10:52:23

IBMdW

2013-12-11 10:00:14

C++新特性C

2025-01-21 08:02:03

2013-10-15 09:48:03

C++Lambda函數(shù)式編程
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)