溫馨提示×

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

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

C++11內(nèi)存模型這么理解

發(fā)布時(shí)間:2022-03-19 10:26:33 來(lái)源:億速云 閱讀:267 作者:iii 欄目:云計(jì)算

本篇內(nèi)容介紹了“C++11內(nèi)存模型這么理解”的有關(guān)知識(shí),在實(shí)際案例的操作過(guò)程中,不少人都會(huì)遇到這樣的困境,接下來(lái)就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

簡(jiǎn)要的歷史回顧

最初,開(kāi)發(fā)者并沒(méi)有發(fā)布一個(gè)公開(kāi)的處理器內(nèi)存模型規(guī)范。然而依據(jù)一組規(guī)則,弱序列化的處理器便可很好的與內(nèi)存進(jìn)行工作。個(gè)人認(rèn)為那會(huì)開(kāi)發(fā)人員肯定希望在未來(lái)的某一天引入一些新的策略(為什么在架構(gòu)開(kāi)發(fā)中需遵循某些規(guī)范?)。然而厄運(yùn)不斷,千兆周就足以讓開(kāi)發(fā)者毛躁。開(kāi)發(fā)者引入多核,最終導(dǎo)致多線程暴增。

最初驚慌的是操作系統(tǒng)開(kāi)發(fā)人員,因?yàn)樗麄儾坏貌痪S護(hù)多核CPU,然而那會(huì)并不存在弱的有序架構(gòu)規(guī)則。此后其它的標(biāo)準(zhǔn)委員會(huì)才陸續(xù)參與進(jìn)來(lái),隨著程序越來(lái)越并行,語(yǔ)言內(nèi)存模型的標(biāo)準(zhǔn)化就應(yīng)運(yùn)而生,為多線程并發(fā)執(zhí)行提供某種保障,不過(guò)現(xiàn)在我們有了處理器內(nèi)存模型規(guī)則。最終,幾乎所有的現(xiàn)代處理器架構(gòu)都有開(kāi)放的內(nèi)存模型規(guī)范。

一直以來(lái)C++就以高級(jí)語(yǔ)言的方式編寫底層代碼的特性而著稱,在C++內(nèi)存模型的開(kāi)發(fā)中自然也是不能破壞這個(gè)特性,必然賦予程序員最大的靈活性。在分析JAVA等語(yǔ)言的內(nèi)存模型,及典型同步原語(yǔ)的內(nèi)部結(jié)構(gòu)和無(wú)鎖算法案例之后,開(kāi)發(fā)人員引入了三種內(nèi)存模型:

  • 序列一致性模型

  • 獲取/釋放語(yǔ)義模型

  • 寬松的內(nèi)存序列化模型(relaxed)

所有這些內(nèi)存模型定義在一個(gè)C++列表中– std::memory_order,包含以下六個(gè)常量:

  • memory_order_seq_cst 指向序列一致性模型

  • memory_order_acquire, memory_order_release, memory_order_acq_rel, memory_order_consume 指向基于獲取/釋放語(yǔ)義的模型

  • memory_order_relaxed 指向?qū)捤傻膬?nèi)存序列化模型

開(kāi)始審視這些模型之前,應(yīng)先確定程序中采用何種內(nèi)存模型,再一次審視原子性運(yùn)算。該運(yùn)算原子性的文章中已有介紹,此運(yùn)算與C++11中定義的運(yùn)算并沒(méi)什么兩樣。因?yàn)槎蓟谶@樣一個(gè)準(zhǔn)則:memory_order作為原子運(yùn)算的參數(shù)。其原因有二:

  1. 語(yǔ)義:事實(shí)上,我們說(shuō)的序列化(內(nèi)存柵障)是指程序執(zhí)行的原子運(yùn)算。位于讀/寫方法中的柵障,其神奇之處就在于跟代碼沒(méi)有關(guān)聯(lián),其實(shí)是柵障等價(jià)存在的指令。另外,讀/寫中的柵障位置取決于架構(gòu)本身。

  2. 實(shí)際應(yīng)用中:英特Itanium是一種特殊的、與眾不同的架構(gòu),該架構(gòu)的序列化內(nèi)存方式,適用于讀寫指令和RMW運(yùn)算。而舊的Itanium版本則是一種可選的指令標(biāo)簽:獲取、釋放或者寬松(relaxed)。但架構(gòu)中不存在單獨(dú)的獲取/釋放語(yǔ)義指令,僅有一個(gè)重量級(jí)的內(nèi)存柵障指令。

下面是真實(shí)的原子性運(yùn)算,std::atomic<T> 類的每種規(guī)范至少應(yīng)包含以下方法:

void store(T, memory_order = memory_order_seq_cst);

T load(memory_order = memory_order_seq_cst) const;

T exchange(T, memory_order = memory_order_seq_cst);

bool compare_exchange_weak(T&, T, memory_order = memory_order_seq_cst);

bool compare_exchange_strong(T&, T, memory_order = memory_order_seq_cst);

獨(dú)立的內(nèi)存柵障

當(dāng)然,在C++11同樣也為大家提供了兩個(gè)獨(dú)立的內(nèi)存柵障方法:

void atomic_thread_fence(memory_order);

void atomic_signal_fence(memory_order);

atomic_thread_fence亦可采用獨(dú)立讀寫柵障的方式運(yùn)行,而后者被告知已過(guò)時(shí)。盡管memory_order序列化方法atomic_signal_fence不提供讀柵障(load/load)或者寫柵障(Strore/Store),不過(guò)atomic_signal_fence可以用于信號(hào)處理器(signal handler);作為一個(gè)規(guī)則,該方法不產(chǎn)生任何代碼,僅是一個(gè)編譯器柵障。(譯者注:稱之為柵欄似乎更為妥當(dāng))。

正如你看到的,缺省狀態(tài)的C++11內(nèi)存模型為序列一致模型,這正是我們要討論的,不過(guò)在這之前我們先簡(jiǎn)要聊聊編譯器柵障。

編譯器柵障

誰(shuí)會(huì)重排我們寫的代碼呢?處理器可以重排代碼,另外還有編譯器。而許多啟發(fā)式開(kāi)發(fā)和優(yōu)化開(kāi)發(fā)方法,都是基于單線程執(zhí)行這樣的假設(shè)。因此,要讓編譯器明白你的代碼是多線程的,那是相當(dāng)困難的。因此它需要提示–柵障。諸如此類的柵障告知編譯器“別把柵障前面的代碼和柵障后面的代碼混在一起,反之亦然”,編譯器柵障不會(huì)產(chǎn)生任何代碼。

MS Visual С++的編譯器柵障是一個(gè)偽方法:_ReadWriteBarrier()。(過(guò)去我一直記不住它的名字:和讀寫內(nèi)存柵障相關(guān)—重量級(jí)內(nèi)存柵障)而對(duì)GCC和Clang而言,它是一個(gè)smart __asm__ __volatile__ ( “” ::: ?memory? )結(jié)構(gòu)。

同樣值得注意的是,assembly __asm__ __volatile__ ( … ) insertions也是一種GCC和Clang柵障。編譯器沒(méi)有權(quán)利遺棄或者重排柵障前后的代碼。C++ memory_order常量,在某種程度上,支持編譯器對(duì)處理器施加影響。作為編譯器柵障,限制了代碼的重排(比如優(yōu)化)。因此,無(wú)需再設(shè)置特定的編譯器柵障,當(dāng)然,前提是編譯器完全支持這一新標(biāo)準(zhǔn)。

序列化一致性模型

假設(shè),我們實(shí)現(xiàn)了一個(gè)無(wú)鎖棧,編譯后并正在進(jìn)行測(cè)試。我們拿到一個(gè)核心文件,會(huì)問(wèn)哪里出錯(cuò)呢?開(kāi)始查找尋找錯(cuò)誤根源,腦袋飛快地在思索無(wú)鎖棧中一行行代碼實(shí)現(xiàn)(沒(méi)有一個(gè)調(diào)試器能幫到我們),試圖模擬多線程,并回答下列問(wèn)題:

“線程1執(zhí)行第k行的同時(shí),線程2執(zhí)行第N行,此時(shí)會(huì)有什么致命問(wèn)題導(dǎo)致程序失敗呢?”或許,你會(huì)發(fā)現(xiàn)錯(cuò)誤根源并處理掉這些錯(cuò)誤,但無(wú)鎖棧依舊報(bào)錯(cuò),這是為何?

(譯者注:所謂核心文件,又叫核心轉(zhuǎn)儲(chǔ),操作系統(tǒng)在進(jìn)程收到某些信號(hào)而終止運(yùn)行時(shí),將此時(shí)進(jìn)程地址空間的內(nèi)容以及有關(guān)進(jìn)程狀態(tài)的其他信息寫入該文件,此信息用來(lái)調(diào)試)

事實(shí)上,我們?cè)噲D尋找錯(cuò)誤根源,在腦海中比較多線程并發(fā)執(zhí)行下的程序不同行,其實(shí)就是序列化一致性。它是一種嚴(yán)格的內(nèi)存模型,確保處理器按照程序本身既定的順序執(zhí)行程序指令,例如,下面的代碼:

// Thread 1

atomic<int> a, b ;

a.store( 5 );

int vb = b.load();

 

// Thread 2

atomic<int> x,y ;

int vx = x.load() ;

y.store( 42 ) ;

任何一種執(zhí)行情形都是序列化一致性模型允許的,除了對(duì)調(diào)換a.store / b.load或者x.load / y.store。注意,我并沒(méi)有顯式地給加載存儲(chǔ)設(shè)置memory_order參數(shù),而是依賴缺省的參數(shù)值。

相同的規(guī)范擴(kuò)展到編譯器:memory_order_seq_cst下面的運(yùn)算不得遷移到此柵障上面,與此同時(shí),在seq_cst-barrier上面的運(yùn)算不得遷移到此柵障下面。

序列化一致性模型接近人腦思維,但它有個(gè)相當(dāng)致命的缺陷,對(duì)現(xiàn)代處理器限制過(guò)多。這會(huì)產(chǎn)生極度重量級(jí)的內(nèi)存柵障,很大程度上制約了處理器的啟發(fā)式執(zhí)行,這也是新的C++標(biāo)準(zhǔn)為何有以下折中的原因:

  • 有序化一致性模型由于其嚴(yán)格的特性,加之容易理解,因而被作為原子運(yùn)算的缺省模型

  • 同時(shí)C++引入一個(gè)弱內(nèi)存柵障,以應(yīng)對(duì)現(xiàn)代架構(gòu)的更多可能

  • 基于獲取/釋放語(yǔ)義的模型作為序列化一致性模型的一個(gè)很好補(bǔ)充。

獲取/釋放語(yǔ)義

正如你看到的標(biāo)題那樣,在某種程度上,該語(yǔ)義與資源的獲取釋放有關(guān)。確實(shí)如此,資源的獲取就是將其從內(nèi)存讀入寄存器,釋放就是將其從寄存器寫回內(nèi)存中。

load memory, register ;

membar #LoadLoad | #LoadStore ; // acquire-барьер

 

// Operation within acquire/release-sections

...

 

membar #LoadStore | #StoreStore ; // release-barrier

store regiser, memory ;

正如你看到的,我們沒(méi)有用到#StoreLoad這樣重量級(jí)柵障應(yīng)用。獲取柵障、釋放柵障就是半個(gè)柵障。獲取不會(huì)將前面的存儲(chǔ)運(yùn)算與后續(xù)的加載、存儲(chǔ)進(jìn)行重排,而釋放不會(huì)將前面加載與后續(xù)的加載進(jìn)行重排,同樣,不會(huì)將前面的存儲(chǔ)與后續(xù)的加載進(jìn)行重排。所有的這些適用于編譯器和處理器,獲取、釋放作為該區(qū)間所有代碼的柵障。而獲取柵障前面的某些運(yùn)算(可以被處理器或編譯器重排)可以滲入到獲取/釋放模塊中。同樣釋放柵障后續(xù)的運(yùn)算可以轉(zhuǎn)入上方進(jìn)入獲取/釋放區(qū)間。但獲取/釋放里面的運(yùn)算不會(huì)越出這個(gè)界。

我猜自旋鎖(spin lock)是獲取/釋放語(yǔ)義應(yīng)用最簡(jiǎn)單的例子。

無(wú)鎖和自旋鎖

或許你會(huì)感到奇怪,在無(wú)鎖算法系列文章中列舉一個(gè)鎖算法的例子似乎不妥,容我解釋一下。

我不是一個(gè)純無(wú)鎖粉,不過(guò),純無(wú)鎖(特別是無(wú)等待)算法確實(shí)令我很開(kāi)心。設(shè)法實(shí)現(xiàn)它我甚至?xí)_(kāi)心。作為一個(gè)務(wù)實(shí)主義者:任何有效的事情就是好的。倘若使用鎖帶來(lái)益處,我也覺(jué)得挺好的。自旋鎖可以帶來(lái)比綜合互斥量(mutex)更多的收益,比如對(duì)一個(gè)小段程序進(jìn)行保護(hù)–少量匯編指令。同樣,針對(duì)不同優(yōu)化,自旋鎖是一種用之不竭的資源。

基于獲取/釋放的最簡(jiǎn)易自旋鎖實(shí)現(xiàn)大致如此:(而C++專家認(rèn)為應(yīng)該用某個(gè)特定的atomic_flag來(lái)實(shí)現(xiàn)自旋鎖,但我更傾向于將自旋鎖建立在原子變量上,甚至不是boolean類型。從本文角度看,這樣看起來(lái)會(huì)更清晰。)

class spin_lock

{

    atomic<unsigned int> m_spin ;

public:

    spin_lock(): m_spin(0) {}

    ~spin_lock() { assert( m_spin.load(memory_order_relaxed) == 0);}

 

    void lock()

    {

        unsigned int nCur;

        do { nCur = 0; }

        while ( !m_spin.compare_exchange_weak( nCur, 1, memory_order_acquire ));

    }

    void unlock()

    {

        m_spin.store( 0, memory_order_release );

    }

};

本代碼中困惑我的是,倘若CAS未成功執(zhí)行,compare_exchange方法,第一參數(shù)接收一個(gè)引用,并修改它。因此不得不采用一個(gè)帶非空體的do-while。

在lock方法中采用獲取-語(yǔ)義,在unlock方法中采用釋放語(yǔ)義(順便說(shuō)一句,獲取/釋放語(yǔ)義來(lái)自同步原語(yǔ),標(biāo)準(zhǔn)開(kāi)發(fā)者細(xì)心地分析各種不同的同步原語(yǔ)實(shí)現(xiàn),進(jìn)而衍生出獲取/釋放模型)。正如早前提到的,本例中的柵障不允許lock和unlock之間的代碼溢出,這正是我們需要的。

原子性m_spin變量確保m_spin=1時(shí),沒(méi)有人可以獲得該鎖,這也是我們所需要的!

大家看到算法中用到了compare_exchange_weak,但它是什么呢?

Weak and Strong CAS

正如你所記得那樣,處理器結(jié)構(gòu)通常會(huì)選擇兩種類型中的一種,或者實(shí)現(xiàn)原子性CAS原語(yǔ),或者實(shí)現(xiàn)LL/SC對(duì)((load-linked/store-conditional)。LL/SC對(duì)可以實(shí)現(xiàn)原子性CAS,但由于很多原因它并不具有原子性。其中一個(gè)原因就是,LL/SC中正在執(zhí)行的代碼可以被操作系統(tǒng)中斷。例如,此刻OS決定將當(dāng)前線程壓出;重新恢復(fù)之后,store-conditional不再響應(yīng)。而CAS會(huì)返回false,錯(cuò)誤的原因不是數(shù)據(jù)本身,而是外部事件-線程被中斷。

正是因?yàn)槿绱?,促使開(kāi)發(fā)人員在標(biāo)準(zhǔn)中添入兩個(gè)compare_exchange原語(yǔ)-弱的和強(qiáng)的。也因此這兩原語(yǔ)分別被命名為compare_exchange_weak和compare_exchange_strong。即使當(dāng)前的變量值等于預(yù)期值,這個(gè)弱的版本也可能失敗,比如返回false??梢?jiàn)任何weak CAS都能破壞CAS語(yǔ)義,并返回false,而它本應(yīng)返回true。而Strong CAS會(huì)嚴(yán)格遵循CAS語(yǔ)義。當(dāng)然,這是值得的。

何種情形下使用Weak CAS,何種情形下使用Strong CAS呢?我做了如下變通:倘若CAS在循環(huán)中(這是一種基本的CAS應(yīng)用模式),循環(huán)中不存在成千上萬(wàn)的運(yùn)算(比如循環(huán)體是輕量級(jí)和簡(jiǎn)單的),我會(huì)使用compare_exchange_weak。否則,采用強(qiáng)類型的compare_exchange_strong。

針對(duì)獲取/釋放語(yǔ)義的內(nèi)存序列

正如上文所述,獲取/釋放語(yǔ)義下的memory_order定義:

  • memory_order_acquire

  • memory_order_consume

  • memory_order_release

  • memory_order_acq_rel

針對(duì)讀(加載),可選memory_order_acquire和 memory_order_consume。針對(duì)寫(存儲(chǔ)),僅能選memory_order_release。Memory_order_acq_rel是唯一可以用來(lái)做RMW運(yùn)算,比如compare_exchange, exchange, fetch_xxx。事實(shí)上,原子性RMW原語(yǔ)擁有獲取語(yǔ)義memory_order_acquire, 釋放語(yǔ)義memory_order_release 或者 memory_order_acq_rel.

這些常量決定了RMW運(yùn)算語(yǔ)義,因?yàn)镽MW原語(yǔ)可以并發(fā)執(zhí)行原子性讀寫。RMW運(yùn)算語(yǔ)義上被認(rèn)為擁有獲取-加載,或者釋放-存儲(chǔ),或者兩者皆有。

只在算法中定義RMW運(yùn)算語(yǔ)義是可行的,在某種程度上與自旋鎖相似的部分,在無(wú)鎖算法中顯得很特別。首先,獲取資源,做一些運(yùn)算,比如計(jì)算新值;最后,釋放掉新的資源值。倘若資源獲取由RMW運(yùn)算(通常為CAS)執(zhí)行,諸如此類的運(yùn)算很有可能擁有獲取語(yǔ)義。倘若某個(gè)新值由RMW原語(yǔ)來(lái)執(zhí)行,此類型很有可能擁有釋放語(yǔ)義。用“很有可能”描述不是沒(méi)有目的,對(duì)算法的具體細(xì)節(jié)進(jìn)行分析是必須的,這樣才能明白什么樣的語(yǔ)義匹配什么樣的RMW運(yùn)算。

倘若RMW原語(yǔ)分開(kāi)執(zhí)行,獲取/釋放模式是做不到的,不過(guò)有三種可能的語(yǔ)義變體:

  • memory_order_seq_cst 是算法的核心, RMW運(yùn)算中,代碼的重排,加載和存儲(chǔ)的上下遷移都會(huì)報(bào)錯(cuò)。

  • memory_order_acq_rel 和memory_order_seq_cst有些相似, 但RMW運(yùn)算位于獲取/釋放內(nèi)部。

  • memory_order_relaxed  RMW運(yùn)算可以上下遷移,不會(huì)引發(fā)錯(cuò)誤。(比如:運(yùn)算就在獲取/釋放區(qū)間)

以上這些細(xì)枝末節(jié)都應(yīng)很好地被理解,然后再試著采用一些基本的原則,在RMW原語(yǔ)上采用這樣的,或那樣的語(yǔ)義。完了之后,必須針對(duì)每個(gè)算法做出細(xì)致地分析。

消費(fèi)語(yǔ)義(COnsume-Semantic)

這是一個(gè)獨(dú)立的,更弱類型的獲取語(yǔ)義,一個(gè)讀消費(fèi)語(yǔ)義。此語(yǔ)義作為一個(gè)“內(nèi)存的禮物”被引入DECAlpha處理器中。Alpha架構(gòu)與其它現(xiàn)代架構(gòu)有很大的不同,它會(huì)破壞數(shù)據(jù)依賴。下面的代碼就是一個(gè)例子:

struct foo {

    int x;

    int y;

} ;

atomic<foo *> pFoo ;

 foo * p = pFoo.load( memory_order_relaxed );

int x = p->x;

重排p->x讀取和p獲?。▌e問(wèn)我這怎么可能呢!這就是Alpha的特點(diǎn)之一,我沒(méi)有用過(guò)Alpha,所以也不能確定這對(duì)與不對(duì))。為了阻止此種重排,引入了消費(fèi)語(yǔ)義,用于struct指針的原子讀,以及struct字段讀取。下面的例子中pFoo指針便是如此:

foo * p = pFoo.load( memory_order_consume );

int x = p->x;

消費(fèi)語(yǔ)義介于讀取的寬松語(yǔ)義和獲取語(yǔ)義之間,現(xiàn)今大多數(shù)架構(gòu)都基于讀取的寬松語(yǔ)義。

再談CAS

我已經(jīng)介紹了兩個(gè)CAS原子性接口-weak和Strong,但不止兩個(gè)CAS變體,其它CAS,多了一個(gè)memory_order參數(shù):

bool compare_exchange_weak(T&, T, memory_order successOrder, memory_order failedOrder );

bool compare_exchange_strong(T&, T, memory_order successOrder, memory_order failedOrder );

不過(guò)failedOrder是什么樣的參數(shù)呢?

記住CAS是RMW原語(yǔ),即便失敗,也會(huì)執(zhí)行原子性讀。CAS失敗,failedOrder參數(shù)會(huì)決定本次讀運(yùn)算語(yǔ)義。普通讀相應(yīng)的相同值也是支持的,在實(shí)際應(yīng)用中,“針對(duì)失敗語(yǔ)義”是極其少有的,當(dāng)然,這取決于算法。

寬松語(yǔ)義

最后,來(lái)說(shuō)說(shuō)第三種原子性模型,寬松語(yǔ)義適用于所有的原子性原語(yǔ)-加載、存儲(chǔ)、所有RMW-幾乎沒(méi)有任何限制。因此,它允許處理器最大程度上的指令重排,這是它最大的優(yōu)勢(shì)。為何是幾乎呢?

首先,該標(biāo)準(zhǔn)需要保證寬松運(yùn)算的原子性。這意味著即使是寬松運(yùn)算也應(yīng)該是原子性的,不存在部分效應(yīng)(partial effects)。

其次,啟發(fā)式寫在原子性寬松寫中是被禁止的。

這些要求會(huì)嚴(yán)格地應(yīng)用于一些弱序列化架構(gòu)的原子性寬松運(yùn)算中。比如,原子性變量的寬松加載在Intel Itanium中由load.acq實(shí)現(xiàn)(acquire-read, 切勿把Itanium acquire和C++ acquire混為一體)。

Itanium之安魂曲

我在文中多次提到英特爾Itanium,搞得我好像就是Intel架構(gòu)粉;其實(shí)該架構(gòu)在慢慢逝去,當(dāng)然我不是英特爾的粉絲。Itanium VLIW 架構(gòu)不同于其它架構(gòu)地方,是其命令系統(tǒng)的構(gòu)建規(guī)則。內(nèi)存序列化由加載、存儲(chǔ)、RMW指令的前綴完成。而在現(xiàn)代架構(gòu)體系中你不會(huì)找到這些的,這些獲取和釋放術(shù)語(yǔ),讓我想到,C++11或許就是從Itanium拷貝過(guò)來(lái)的。

過(guò)去,我們一直在用Itanium或者它的子架構(gòu),直到AMD引入AMD64—將x86擴(kuò)展到64位。那時(shí)Intel正慢悠悠地開(kāi)發(fā)一款64位計(jì)算架構(gòu)。這個(gè)架構(gòu)潛藏著一些細(xì)枝末節(jié),透過(guò)它,你會(huì)了解到臺(tái)式機(jī)Itanium原本是為我們準(zhǔn)備的。另外,針對(duì)Itanium架構(gòu)的微軟Windows操作系統(tǒng)端口和Visual C++編譯器也間接地證明這一點(diǎn)(還有人看到其它運(yùn)行在Itanium上的Windows操作系統(tǒng)嗎?)。顯然AMD打亂了Intel的計(jì)劃,而Intel必須迎頭趕上,將64位整合進(jìn)x86。最后,Itanium停留在服務(wù)器片段中,因拿不到合適的開(kāi)發(fā)資源,而慢慢消失了。

不過(guò),Itanium的一組VLIW指令卻是很有趣,并已取得突破性進(jìn)展?,F(xiàn)代處理器執(zhí)行的這些指令(加載執(zhí)行塊,重排運(yùn)算)曾經(jīng)被植入Itanium 的編譯器中。但該編譯器不能處理任務(wù),也不能產(chǎn)生完備的優(yōu)化代碼。結(jié)果,Itanium性能數(shù)次跌入谷底,因此Itanium 是我們不可以實(shí)現(xiàn)的未來(lái)。

但有誰(shuí)知道呢,或許現(xiàn)在寫夢(mèng)之安魂曲為時(shí)尚早?

熟悉C++11標(biāo)準(zhǔn)的人肯定會(huì)問(wèn):“關(guān)系(relations)在何處決定原子性運(yùn)算語(yǔ)義:happened before, synchronized with ,還是其它?”我會(huì)說(shuō)“在標(biāo)準(zhǔn)里”。

Anthony Williams在其書《C++ Concurrency in Action》第五章對(duì)此有詳盡的描述,你可以找到很多詳盡的例子。

標(biāo)準(zhǔn)開(kāi)發(fā)者有一項(xiàng)重要的任務(wù),對(duì)C++內(nèi)存模型規(guī)則做一些變動(dòng)。該規(guī)則不是用來(lái)描述內(nèi)存柵障的位置,而是用來(lái)保障線程之間通信的。

結(jié)果,一個(gè)簡(jiǎn)潔明了的C++內(nèi)存模型規(guī)范就此產(chǎn)生了。

不幸的是,在實(shí)際應(yīng)用中,此關(guān)系使用起來(lái)太過(guò)困難;不論是在復(fù)雜或是簡(jiǎn)易的無(wú)鎖算法中,大量的變量需要考慮,才能保證memory_order的正確性。

這就是為何缺省模型為序列化一致性模型,它無(wú)需針對(duì)原子性運(yùn)算設(shè)置任何特殊的memory_order參數(shù)。前面已經(jīng)提到,該模型處于一種減速狀態(tài),應(yīng)用弱模型—比如獲取/釋放 或者寬松—均需要算法驗(yàn)證。

補(bǔ)充說(shuō)明:讀了一些文章發(fā)現(xiàn)最后的論述不夠準(zhǔn)確。事實(shí)上,序列一致性模型本身不保證任何事情,即使有它的幫助,你也能把代碼寫的一團(tuán)糟。因此不論何種內(nèi)存模型,無(wú)鎖算法驗(yàn)證都是必須的。只不過(guò)在弱模型中,特別有必要。

無(wú)鎖算法驗(yàn)證

我知道的第一個(gè)驗(yàn)證方式,是Dmitriy Vyukov寫的 relacy 庫(kù)。不幸的是,該方式需要建立一個(gè)特殊模型。第一步,簡(jiǎn)化的無(wú)鎖模型應(yīng)該以relacy library方式來(lái)構(gòu)建;而且該模型應(yīng)該經(jīng)過(guò)調(diào)試(為何是簡(jiǎn)化的呢?在建模的時(shí)候,通常你要深思熟慮摒棄掉跟算法無(wú)關(guān)的東西);只有這樣,你才能寫出一個(gè)算法產(chǎn)品。該方式特別適合從事無(wú)鎖算法和無(wú)鎖數(shù)據(jù)結(jié)構(gòu)開(kāi)發(fā)的軟件工程師,事實(shí)上也確實(shí)如此。

但通常很難做到兩步,或許是人惰性的天性,他們即可馬上就需出東西。

我猜relacy的作者也意識(shí)到這個(gè)缺陷(不是嘲諷,在這個(gè)小領(lǐng)域也算是一個(gè)突破性的項(xiàng)目)。作者將一個(gè)驗(yàn)證方法作為標(biāo)準(zhǔn)庫(kù)的一部分,這也意味著你無(wú)須做任何額外的模型。這個(gè)看起來(lái)有些像STL中的safe iterators概念。

最近一個(gè)新工具ThreadSanitizer由Dmitriy和他谷歌的同事一起開(kāi)發(fā)的,這個(gè)工具可以用來(lái)檢測(cè)程序中存在的數(shù)據(jù)競(jìng)爭(zhēng);因此在原子性運(yùn)算的重排中非常有用。更重要的是,該工具不是構(gòu)建進(jìn)了STL,而是更底層的編譯器中(比如Clang3.2、GCC4.8)。

ThreadSanitizer的使用方式特別簡(jiǎn)單,編譯某個(gè)程序時(shí)僅僅需要特定的按鍵,運(yùn)行單元測(cè)試,接著就可以看到豐富的日志分析結(jié)構(gòu)。因此,我也將本工具應(yīng)用于我的libcds庫(kù)中,確保libsds沒(méi)有問(wèn)題。

“我不是很明白”—批判標(biāo)準(zhǔn)

我斗膽批判C++標(biāo)準(zhǔn),只是不明白為何標(biāo)準(zhǔn)將該語(yǔ)義設(shè)置為原子性運(yùn)算的參數(shù)。不過(guò),邏輯上應(yīng)該使用模板,這么做才對(duì)嘛:

template <typename T>

class atomic {

    template <memory_order Order = memory_order_seq_cst>

    T load() const ;

 

    template <memory_order Order = memory_order_seq_cst>

    void store( T val ) ;

 

    template <memory_order SuccessOrder = memory_order_seq_cst>

    bool compare_exchange_weak( T& expected, T desired ) ;

 

   // and so forth, and so on

};

我來(lái)談?wù)劄楹挝业南敕ǜ_呢。

前面不止一次提到,原子性運(yùn)算語(yǔ)義不僅作用于處理器,也作用于編譯器。語(yǔ)義是編譯器的優(yōu)化(半)柵障。除此之外,編譯器應(yīng)該監(jiān)控原子性運(yùn)算是否被賦予恰當(dāng)語(yǔ)義(比如,釋放語(yǔ)義應(yīng)用于讀運(yùn)算)。那該語(yǔ)義在編譯期就應(yīng)該確定下來(lái),但在下面的代碼中,我很難想象編譯器如何做到這一點(diǎn):

從形式上看,該代碼并不違反C++11標(biāo)準(zhǔn),不過(guò)編譯器唯一能做的也就只有下面這些了:

extern std::memory_order currentOrder ;

std::Atomic<unsigned int> atomicInt ;

atomicInt.store( 42, currentOrder ) ;

要么報(bào)錯(cuò),但為何允許原子性運(yùn)算接口拋出錯(cuò)誤?

要么應(yīng)用序列化一致性語(yǔ)義,總之是“不會(huì)太糟”。但變量currentOrder會(huì)被忽略掉,程序會(huì)遇到很多我們?cè)鞠氡苊獾膯?wèn)題。

要么產(chǎn)生一個(gè)針對(duì)所有currentOrder可能值的switch/case語(yǔ)句。但這樣,我們會(huì)得到很多低效的代碼,而非一兩個(gè)匯編指令。恰當(dāng)語(yǔ)義問(wèn)題還未解決,你可以調(diào)用釋放讀或者獲取寫。

然而模板方式卻沒(méi)有此缺陷,在模板函數(shù)中,memory_order列表中定義編譯期常量。的確,原子性運(yùn)算調(diào)用確實(shí)有些繁瑣。

std::Atomic<int> atomicInt ;

atomicInt.store<std::memory_order_release>( 42 ) ;

// or even like that:

atomicInt.template store<std::memory_order_release>( 42 ) ;

但這些繁瑣可以借由模板方式抵消,在編譯期運(yùn)算語(yǔ)義就可以明白無(wú)誤地顯示出來(lái)。而C++采用非模板的方式唯一的解釋就是為了兼容C語(yǔ)言。除了std::atomic類,C++11標(biāo)準(zhǔn)還引入了諸如 atomic_load, atomic_store等C原子性函數(shù)。

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

向AI問(wèn)一下細(xì)節(jié)

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

c++
AI