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