最近看了《七周七并發(fā)模型》[1]趟脂,對(duì)自己熟悉的C/C++并發(fā)編程有了很多新的思考泰讽。在Google上搜索“C C++ 并發(fā) 編程”,結(jié)果主要是Anthony的《C++ Concurrency in Action》以及零散的一些博文昔期。Anthony的書(shū)主要是教授C++最基礎(chǔ)的線程與鎖模型和無(wú)鎖編程的知識(shí)已卸,但是其它的并發(fā)模型書(shū)中并未提及。線程與鎖模型因其資料豐富“簡(jiǎn)單易學(xué)”被廣大C/C++程序員所使用硼一。該模型導(dǎo)致的死鎖累澡、饑餓等等問(wèn)題也是大家很頭痛的事情。實(shí)際上對(duì)于C/C++并發(fā)模型般贼,我們還有很多其它的選擇愧哟,比如Actor、CSP哼蛆、協(xié)程等蕊梧,而這正是這個(gè)C/++并發(fā)編程系列要告訴大家的。開(kāi)篇先說(shuō)一下并發(fā)編程的基礎(chǔ)知識(shí)腮介,并發(fā)與并行的區(qū)別和C/C++多線程內(nèi)存模型肥矢。
并發(fā)與并行的區(qū)別?
網(wǎng)絡(luò)上有很多關(guān)于“并發(fā)”與“并行”的解釋叠洗,大家比較認(rèn)同的是Golang大神Rob Pike在“并發(fā)不是并行[3]”的技術(shù)分享上的解釋:
Concurrency vs. parallelism
Concurrency is about dealing with lots of things at once.
Parallelism is about doing lots of things at once.
Not the same, but related.
Concurrency is about structure, parallelism is about execution.并發(fā)關(guān)乎結(jié)構(gòu)甘改,并行關(guān)乎執(zhí)行。
Concurrency provides a way to structure a solution to solve a problem that may (but not necessarily) be parallelizable.并發(fā)提供了一種方式讓我們能夠設(shè)計(jì)一種方案將問(wèn)題(非必須的)并行的解決灭抑。[2]
按我個(gè)人對(duì)以上的理解十艾,“并行”和“并發(fā)”的區(qū)別,可以簡(jiǎn)單理解為“并行 = 并發(fā)執(zhí)行”腾节。不管是多線程程序忘嫉、多進(jìn)程程序荤牍,在設(shè)計(jì)和實(shí)現(xiàn)階段應(yīng)該稱之為“并發(fā)”,而運(yùn)行時(shí)應(yīng)該稱之為“并行”榄融〔我可以類比我們熟悉的“程序 vs. 進(jìn)程”,運(yùn)行時(shí)的程序稱之為進(jìn)程愧杯。它們都是對(duì)同一個(gè)事物處在不同階段/狀態(tài)時(shí)的定義涎才。
C/C++多線程內(nèi)存模型
以前我認(rèn)為內(nèi)存模型和內(nèi)存布局是一回事,比如Linux下ELF可執(zhí)行文件格式力九,堆耍铜、棧、.data段跌前、.text段等等棕兼。實(shí)際上ELF這樣的內(nèi)存布局格式是Linux操作系統(tǒng)對(duì)可執(zhí)行程序的規(guī)范,不管用什么編程語(yǔ)言生成了直接(依賴運(yùn)行時(shí)“虛擬機(jī)”的語(yǔ)言除外)可運(yùn)行的程序抵乓,最終都是ELF的內(nèi)存布局伴挚。而內(nèi)存模型是編程語(yǔ)言和計(jì)算機(jī)系統(tǒng)(包括編譯器,多核CPU等可能對(duì)程序進(jìn)行亂序優(yōu)化的軟硬件)之間的契約灾炭,它規(guī)定了多個(gè)線程訪問(wèn)同一個(gè)內(nèi)存位置時(shí)的語(yǔ)義茎芋,以及某個(gè)線程對(duì)內(nèi)存位置的更新何時(shí)能被其它線程看見(jiàn)[4]。
在C11/C++11標(biāo)準(zhǔn)之前蜈出,C/C++語(yǔ)言沒(méi)有內(nèi)存模型的定義田弥。在此期間,我們天真的認(rèn)為程序是按順序一致性(Sequential consistency)模型去運(yùn)行的铡原,而實(shí)際上編譯器和多核CPU卻是不滿足順序一致性模型的偷厦。Leslie在其論文[6]中定義了順序一致性模型需要滿足的兩個(gè)條件:
Rl: Each processor issues memory requests in the order specified by its program.
R2: Memory requests from all processors issued to an individual memory module are serviced from a single FIFO queue. Issuing a memory request consists of entering the request on this queue.
條件“R1”可以理解為“單個(gè)線程內(nèi)指令的執(zhí)行順序和代碼的順序是一致的”,而條件“R2”則讓多線程的指令執(zhí)行順序從全局來(lái)看是“串行”執(zhí)行的⊙嗫蹋現(xiàn)代CPU的緩存只泼、流水線和亂序執(zhí)行機(jī)制以及編譯器的代碼優(yōu)化、重排都無(wú)法滿足順序一致性模型卵洗。所以请唱,機(jī)器實(shí)際執(zhí)行的代碼并不是你寫(xiě)的代碼[9]。
為了在性能和易編程性之間找到平衡忌怎,C++11提出了“sequential consistency for data race free programs”內(nèi)存模型,即沒(méi)有數(shù)據(jù)競(jìng)跑(data race)的程序符合順序一致性酪夷。數(shù)據(jù)競(jìng)跑是指多個(gè)線程在沒(méi)有同步的情況下去訪問(wèn)相同的內(nèi)存位置[5]榴啸。所以,在C11/C++11后晚岭,我們只要對(duì)多線程之間需要同步的變量和操作鸥印,使用正確的同步原語(yǔ)進(jìn)行同步,就能保證程序的執(zhí)行符合順序一致性。編譯器库说、多核CPU能保證其優(yōu)化措施不會(huì)破壞順序一致性狂鞋。
理論有些晦澀,我引用個(gè)例子說(shuō)明潜的。如下:
x = y = 0;
Thread1 Thread2
x = 1; y = 1;
r1 = y; r2 = x;
按照順序一致性模型骚揍,會(huì)有以下5種可能的執(zhí)行順序
從分析來(lái)看是不會(huì)出現(xiàn)“r1 = 0,r2 = 0”的情況的啰挪。但是C11/C++11之前并未規(guī)定多線程內(nèi)存模型信不,也沒(méi)有多線程的標(biāo)準(zhǔn)庫(kù)。pthread多線程庫(kù)是按照“單線程執(zhí)行模型(Single thread execution model)”來(lái)實(shí)現(xiàn)的亡呵。從編譯器的角度來(lái)看抽活,不存在什么多線程這樣的東西,程序就是一個(gè)代碼序列锰什。只要編譯優(yōu)化措施不影響順序執(zhí)行的結(jié)果下硕,就可以執(zhí)行這項(xiàng)優(yōu)化。比如下面這種優(yōu)化:
6
Thread1 Thread2
r1 = y;
y = 1;
r2 = x;
x = 1;
r1 = 0汁胆,r2 = 0
Thread1內(nèi)的“r1 = y”被換到了“x = 1”之前梭姓,這在C11/C++11標(biāo)準(zhǔn)之前是可能發(fā)生的。因?yàn)榘磫尉€程執(zhí)行模型沦泌,“給x賦值1”與“讀取y賦值給r1”是兩個(gè)不相關(guān)的事情糊昙,調(diào)換執(zhí)行順序不影響最終結(jié)果。而對(duì)于C11/C++11標(biāo)準(zhǔn)來(lái)說(shuō)谢谦,因?yàn)檫@段代碼不存在數(shù)據(jù)競(jìng)跑释牺,只要使用標(biāo)準(zhǔn)庫(kù)提供的線程操作來(lái)實(shí)現(xiàn),其執(zhí)行就符合順序一致性回挽,不會(huì)優(yōu)化出現(xiàn)“6”這種情況没咙。
另外,C11/C++11標(biāo)準(zhǔn)還明確了“內(nèi)存位置”的定義千劈。
一個(gè)內(nèi)存位置要么是標(biāo)量祭刚,要么是一組緊鄰的具有非零長(zhǎng)度的位域。
兩個(gè)線程可以互不干擾地對(duì)不同的內(nèi)存位置進(jìn)行讀寫(xiě)操作
比如有如下的結(jié)構(gòu)體:
struct
{
int a : 17;
int b : 15;
} x;
兩個(gè)線程分別讀寫(xiě)a和b墙牌,是否會(huì)互相干擾呢涡驮?畢竟CPU是按32/64位來(lái)取操作數(shù)的,而不是按17/15位來(lái)的喜滨。C11/C++11之前這樣的操作是未定義的捉捅,按C11/C++標(biāo)準(zhǔn)規(guī)定a和b則屬于同一個(gè)內(nèi)存位置。兩個(gè)線程分別對(duì)a虽风、b進(jìn)行讀寫(xiě)操作是會(huì)相互干擾的棒口,需要進(jìn)行同步寄月。或者將a无牵、b分割成兩個(gè)內(nèi)存位置:
struct
{
int a : 17; // 內(nèi)存位置1
int : 0;
int b : 15; // 內(nèi)存位置2
} x;
這樣編譯器會(huì)自動(dòng)自行內(nèi)存對(duì)齊漾肮,保證兩個(gè)線程分別讀寫(xiě)a、b互不干擾茎毁。
參考
[1]《七周七并發(fā)模型》克懊,Paul Butcher 著,黃炎 譯
[2] 也談并發(fā)與并行充岛,Tony Bai
[3] Concurrency is not parallelism保檐,Rob Pike
[4] 淺析C++多線程內(nèi)存模型,Guancheng (G.C.)
[5] Race Condition vs. Data Race崔梗,John Regehr
[6] How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs夜只,1979,Leslie Lamport
[7] 《C++0x漫談》系列之:多線程內(nèi)存模型蒜魄,劉未鵬
[8] ISO/IEC JTC1 SC22 WG21 N3690扔亥,Programming Languages — C++
[9] C++ Memory Model,Valentin .etc
修訂記錄
2017-11-01 AM:從網(wǎng)易博客遷移到簡(jiǎn)書(shū)
2017-09-30 PM:完成初稿
版權(quán)聲明:自由轉(zhuǎn)載-非商用-非衍生-保持署名(創(chuàng)意共享3.0許可證)