這里從兩方面來講內(nèi)存模型:一方面是基本結(jié)構(gòu),這與事務(wù)在內(nèi)存中是怎樣布局的有關(guān);另一方面就是并發(fā)。對(duì)于并發(fā)基本結(jié)構(gòu)很重要,特別是在低層原子操作。所以我將會(huì)從基本結(jié)構(gòu)講起。C++
中它與所有的對(duì)象和內(nèi)存位置有關(guān)。
在一個(gè)C++
程序中的所有數(shù)據(jù)都是由對(duì)象(objects)構(gòu)成。這不是說你可以創(chuàng)建一個(gè)int的衍生類,或者是基本類型中存在有成員函數(shù),或是像在Smalltalk和Ruby語言下討論程序那樣——“一切都是對(duì)象”?!皩?duì)象”僅僅是對(duì)C++數(shù)據(jù)構(gòu)建塊的一個(gè)聲明。C++
標(biāo)準(zhǔn)定義類對(duì)象為“存儲(chǔ)區(qū)域”,但對(duì)象還是可以將自己的特性賦予其他對(duì)象,比如,其類型和生命周期。
像int或float這樣的對(duì)象就是簡(jiǎn)單基本類型;當(dāng)然,也有用戶定義類的實(shí)例。一些對(duì)象(比如,數(shù)組,衍生類的實(shí)例,特殊(具有非靜態(tài)數(shù)據(jù)成員)類的實(shí)例)擁有子對(duì)象,但是其他對(duì)象就沒有。
無論對(duì)象是怎么樣的一個(gè)類型,一個(gè)對(duì)象都會(huì)存儲(chǔ)在一個(gè)或多個(gè)內(nèi)存位置上。每一個(gè)內(nèi)存位置不是一個(gè)標(biāo)量類型的對(duì)象,就是一個(gè)標(biāo)量類型的子對(duì)象,比如,unsigned short、my_class*或序列中的相鄰位域。當(dāng)你使用位域,就需要注意:雖然相鄰位域中是不同的對(duì)象,但仍視其為相同的內(nèi)存位置。如圖5.1所示,將一個(gè)struct分解為多個(gè)對(duì)象,并且展示了每個(gè)對(duì)象的內(nèi)存位置。
http://wiki.jikexueyuan.com/project/cplusplus-concurrency-action/images/chapter5/5-1.png" alt="" />
圖5.1 分解一個(gè)struct,展示不同對(duì)象的內(nèi)存位置
首先,完整的struct是一個(gè)有多個(gè)子對(duì)象(每一個(gè)成員變量)組成的對(duì)象。位域bf1和bf2共享同一個(gè)內(nèi)存位置(int是4字節(jié)、32位類型),并且std::string
類型的對(duì)象s由內(nèi)部多個(gè)內(nèi)存位置組成,但是其他的每個(gè)成員都擁有自己的內(nèi)存位置。注意,位域?qū)挾葹?的bf3是如何與bf4分離,并擁有各自的內(nèi)存位置的。(譯者注:圖中bf3是一個(gè)錯(cuò)誤展示,在C++
和C中規(guī)定,寬度為0的一個(gè)未命名位域強(qiáng)制下一位域?qū)R到其下一type邊界,其中type是該成員的類型。這里使用命名變量為0的位域,可能只是想展示其與bf4是如何分離的。有關(guān)位域的更多可以參考wiki的頁面)。
這里有四個(gè)需要牢記的原則:
我確定你會(huì)好奇,這些在并發(fā)中有什么作用,那么下面就讓我們來見識(shí)一下。
這部分對(duì)于C++
的多線程應(yīng)用來說是至關(guān)重要的:所有東西都在內(nèi)存中。當(dāng)兩個(gè)線程訪問不同的內(nèi)存位置時(shí),不會(huì)存在任何問題,一切都工作順利。而另一種情況下,當(dāng)兩個(gè)線程訪問同一個(gè)內(nèi)存位置,你就要小心了。如果沒有線程更新內(nèi)存位置上的數(shù)據(jù),那還好;只讀數(shù)據(jù)不需要保護(hù)或同步。當(dāng)有線程對(duì)內(nèi)存位置上的數(shù)據(jù)進(jìn)行修改,那就有可能會(huì)產(chǎn)生條件競(jìng)爭(zhēng),就如第3章所述的那樣。
為了避免條件競(jìng)爭(zhēng),兩個(gè)線程就需要一定的執(zhí)行順序。第一種方式,如第3章所述那樣,使用互斥量來確定訪問的順序;當(dāng)同一互斥量在兩個(gè)線程同時(shí)訪問前被鎖住,那么在同一時(shí)間內(nèi)就只有一個(gè)線程能夠訪問到對(duì)應(yīng)的內(nèi)存位置,所以后一個(gè)訪問必須在前一個(gè)訪問之后。另一種方式是使用原子操作同步機(jī)制(詳見5.2節(jié)中對(duì)于原子操作的定義),決定兩個(gè)線程的訪問順序。使用原子操作來規(guī)定順序在5.3節(jié)中會(huì)有介紹。當(dāng)多于兩個(gè)線程訪問同一個(gè)內(nèi)存地址時(shí),對(duì)每個(gè)訪問這都需要定義一個(gè)順序。
如果不去規(guī)定兩個(gè)不同線程對(duì)同一內(nèi)存地址訪問的順序,那么訪問就不是原子的;并且,當(dāng)兩個(gè)線程都是“作者”時(shí),就會(huì)產(chǎn)生數(shù)據(jù)競(jìng)爭(zhēng)和未定義行為。
以下的聲明由為重要:未定義的行為是C++
中最黑暗的角落。根據(jù)語言的標(biāo)準(zhǔn),一旦應(yīng)用中有任何未定義的行為,就很難預(yù)料會(huì)發(fā)生什么事情;因?yàn)?,未定義行為是難以預(yù)料的。我就知道一個(gè)未定義行為的特定實(shí)例,讓某人的顯示器起火的案例。雖然,這種事情應(yīng)該不會(huì)發(fā)生在你身上,但是數(shù)據(jù)競(jìng)爭(zhēng)絕對(duì)是一個(gè)嚴(yán)重的錯(cuò)誤,并且需要不惜一切代價(jià)避免它。
另一個(gè)重點(diǎn)是:當(dāng)程序中的對(duì)同一內(nèi)存地址中的數(shù)據(jù)訪問存在競(jìng)爭(zhēng),你可以使用原子操作來避免未定義行為。當(dāng)然,這不會(huì)影響競(jìng)爭(zhēng)的產(chǎn)生——原子操作并沒有指定訪問順序——但原子操作把程序拉回了定義行為的區(qū)域內(nèi)。
在我們了解原子操作前,還有一個(gè)有關(guān)對(duì)象和內(nèi)存地址的概念需要重點(diǎn)了解:修改順序。
每一個(gè)在C++程序中的對(duì)象,都有(由程序中的所有線程對(duì)象)確定好的修改順序,在的初始化開始階段確定。在大多數(shù)情況下,這個(gè)順序不同于執(zhí)行中的順序,但是在給定的執(zhí)行程序中,所有線程都需要遵守這順序。如果對(duì)象不是一個(gè)原子類型(將在5.2節(jié)詳述),你必要確保有足夠的同步操作,來確定每個(gè)線程都遵守了變量的修改順序。當(dāng)不同線程在不同序列中訪問同一個(gè)值時(shí),你可能就會(huì)遇到數(shù)據(jù)競(jìng)爭(zhēng)或未定義行為(詳見5.1.2節(jié))。如果你使用原子操作,編譯器就有責(zé)任去替你做必要的同步。
這一要求意味著:投機(jī)執(zhí)行是不允許的,因?yàn)楫?dāng)線程按修改順序訪問一個(gè)特殊的輸入,之后的讀操作,必須由線程返回較新的值,并且之后的寫操作必須發(fā)生在修改順序之后。同樣的,在同一線程上允許讀取對(duì)象的操作,要不返回一個(gè)已寫入的值,要不在對(duì)象的修改順序后(也就是在讀取后)再寫入另一個(gè)值。雖然,所有線程都需要遵守程序中每個(gè)獨(dú)立對(duì)象的修改順序,但它們沒有必要遵守在獨(dú)立對(duì)象上的相對(duì)操作順序。在5.3.3節(jié)中會(huì)有更多關(guān)于不同線程間操作順序的內(nèi)容。
所以,什么是原子操作?它如何來規(guī)定順序?接下來的一節(jié)中,會(huì)為你揭曉答案。