最簡單和最基本的并發(fā),是指兩個(gè)或更多獨(dú)立的活動(dòng)同時(shí)發(fā)生。
并發(fā)在生活中隨處可見,我們可以一邊走路一邊說話,也可以兩只手同時(shí)作不同的動(dòng)作,還有我們每個(gè)人都過著相互獨(dú)立的生活——當(dāng)我在游泳的時(shí)候,你可以看球賽,等等。
計(jì)算機(jī)領(lǐng)域的并發(fā)指的是在單個(gè)系統(tǒng)里同時(shí)執(zhí)行多個(gè)獨(dú)立的任務(wù),而非順序的進(jìn)行一些活動(dòng)。
計(jì)算機(jī)領(lǐng)域里,并發(fā)不是一個(gè)新事物:很多年前,一臺(tái)計(jì)算機(jī)就能通過多任務(wù)操作系統(tǒng)的切換功能,同時(shí)運(yùn)行多個(gè)應(yīng)用程序;高端多處理器服務(wù)器在很早就已經(jīng)實(shí)現(xiàn)了真正的并行計(jì)算。那“老東西”上有哪些“新東西”能讓它在計(jì)算機(jī)領(lǐng)域越來越流行呢?——真正任務(wù)并行,而非一種錯(cuò)覺。
以前,大多數(shù)計(jì)算機(jī)只有一個(gè)處理器,具有單個(gè)處理單元(processing unit)或核心(core),如今還有很多這樣的臺(tái)式機(jī)。這種機(jī)器只能在某一時(shí)刻執(zhí)行一個(gè)任務(wù),不過它可以每秒進(jìn)行多次任務(wù)切換。通過“這個(gè)任務(wù)做一會(huì),再切換到別的任務(wù),再做一會(huì)兒”的方式,讓任務(wù)看起來是并行執(zhí)行的。這種方式稱為任務(wù)切換。如今,我們?nèi)匀粚⑦@樣的系統(tǒng)稱為并發(fā):因?yàn)槿蝿?wù)切換得太快,以至于無法感覺到任務(wù)在何時(shí)會(huì)被暫時(shí)掛起,而切換到另一個(gè)任務(wù)。任務(wù)切換會(huì)給用戶和應(yīng)用程序造成一種“并發(fā)的假象”。因?yàn)檫@種假象,當(dāng)應(yīng)用在任務(wù)切換的環(huán)境下和真正并發(fā)環(huán)境下執(zhí)行相比,行為還是有著微妙的不同。特別是對內(nèi)存模型不正確的假設(shè)(詳見第5章),在多線程環(huán)境中可能不會(huì)出現(xiàn)(詳見第10章)。
多處理器計(jì)算機(jī)用于服務(wù)器和高性能計(jì)算已有多年?;趩涡径嗪颂幚砥?多核處理器)的臺(tái)式機(jī),也越來越大眾化。無論擁有幾個(gè)處理器,這些機(jī)器都能夠真正的并行多個(gè)任務(wù)。我們稱其為硬件并發(fā)(hardware concurrency)”。
圖1.1顯示了一個(gè)計(jì)算機(jī)處理恰好兩個(gè)任務(wù)時(shí)的理想情景,每個(gè)任務(wù)被分為10個(gè)相等大小的塊。在一個(gè)雙核機(jī)器(具有兩個(gè)處理核心)上,每個(gè)任務(wù)可以在各自的處理核心上執(zhí)行。在單核機(jī)器上做任務(wù)切換時(shí),每個(gè)任務(wù)的塊交織進(jìn)行。但它們中間有一小段分隔(圖中所示灰色分隔條的厚度大于雙核機(jī)器的分隔條);為了實(shí)現(xiàn)交織進(jìn)行,系統(tǒng)每次從一個(gè)任務(wù)切換到另一個(gè)時(shí)都需要切換一次上下文(context switch),任務(wù)切換也有時(shí)間開銷。進(jìn)行上下文的切換時(shí),操作系統(tǒng)必須為當(dāng)前運(yùn)行的任務(wù)保存CPU的狀態(tài)和指令指針,并計(jì)算出要切換到哪個(gè)任務(wù),并為即將切換到的任務(wù)重新加載處理器狀態(tài)。然后,CPU可能要將新任務(wù)的指令和數(shù)據(jù)的內(nèi)存載入到緩存中,這會(huì)阻止CPU執(zhí)行任何指令,從而造成的更多的延遲。
http://wiki.jikexueyuan.com/project/cplusplus-concurrency-action/images/chapter1/1-1.png" alt="" />
圖 1.1 并發(fā)的兩種方式:雙核機(jī)器的真正并行 Vs. 單核機(jī)器的任務(wù)切換
有些處理器可以在一個(gè)核心上執(zhí)行多個(gè)線程,但硬件并發(fā)在多處理器或多核系統(tǒng)上效果更加顯著。硬件線程最重要的因素是數(shù)量,也就是硬件上可以并發(fā)運(yùn)行多少獨(dú)立的任務(wù)。即便是具有真正硬件并發(fā)的系統(tǒng),也很容易擁有比硬件“可并行最大任務(wù)數(shù)”還要多的任務(wù)需要執(zhí)行,所以任務(wù)切換在這些情況下仍然適用。例如,在一個(gè)典型的臺(tái)式計(jì)算機(jī)上可能會(huì)有成百上千個(gè)的任務(wù)在運(yùn)行,即便是在計(jì)算機(jī)處于空閑時(shí),還是會(huì)有后臺(tái)任務(wù)在運(yùn)行。正是任務(wù)切換使得這些后臺(tái)任務(wù)可以運(yùn)行,并使得你可以同時(shí)運(yùn)行文字處理器、編譯器、編輯器和web瀏覽器(或其他應(yīng)用的組合)。圖1.2顯示了四個(gè)任務(wù)在雙核處理器上的任務(wù)切換,仍然是將任務(wù)整齊地劃分為同等大小塊的理想情況。實(shí)際上,許多因素會(huì)使得分割不均和調(diào)度不規(guī)則。部分因素將在第8章中討論,那時(shí)我們再來看一看影響并行代碼性能的因素。
無論應(yīng)用程序在單核處理器,還是多核處理器上運(yùn)行;也不論是任務(wù)切換還是真正的硬件并發(fā),這里提到的技術(shù)、功能和類(本書所涉及的)都能使用得到。如何使用并發(fā),將很大程度上取決于可用的硬件并發(fā)。我們將在第8章中再次討論這個(gè)問題,并具體研究C++代碼并行設(shè)計(jì)的問題。
http://wiki.jikexueyuan.com/project/cplusplus-concurrency-action/images/chapter1/1-2.png" alt="" />
圖 1.2 四個(gè)任務(wù)在兩個(gè)核心之間的切換
試想當(dāng)兩個(gè)程序員在兩個(gè)獨(dú)立的辦公室一起做一個(gè)軟件項(xiàng)目,他們可以安靜地工作、不互相干擾,并且他們?nèi)耸忠惶讌⒖际謨?。但是,他們溝通起來就有些困難,比起可以直接互相交談,他們必須使用電話、電子郵件或到對方的辦公室進(jìn)行直接交流。并且,管理兩個(gè)辦公室需要有一定的經(jīng)費(fèi)支出,還需要購買多份參考手冊。
假設(shè),讓開發(fā)人員同在一間辦公室辦公,他們可以自由的對某個(gè)應(yīng)用程序設(shè)計(jì)進(jìn)行討論,也可以在紙或白板上輕易的繪制圖表,對設(shè)計(jì)觀點(diǎn)進(jìn)行輔助性闡釋。現(xiàn)在,你只需要管理一個(gè)辦公室,只要有一套參考資料就夠了。遺憾的是,開發(fā)人員可能難以集中注意力,并且還可能存在資源共享的問題(比如,“參考手冊哪去了?”)
以上兩種方法,描繪了并發(fā)的兩種基本途徑。每個(gè)開發(fā)人員代表一個(gè)線程,每個(gè)辦公室代表一個(gè)進(jìn)程。第一種途徑是每個(gè)進(jìn)程只要一個(gè)線程,這就類似讓每個(gè)開發(fā)人員擁有自己的辦公室,而第二種途徑是每個(gè)進(jìn)程有多個(gè)線程,如同一個(gè)辦公室里有兩個(gè)開發(fā)人員。讓我們在一個(gè)應(yīng)用程序中簡單的分析一下這兩種途徑。
使用并發(fā)的第一種方法,是將應(yīng)用程序分為多個(gè)獨(dú)立的進(jìn)程,它們在同一時(shí)刻運(yùn)行,就像同時(shí)進(jìn)行網(wǎng)頁瀏覽和文字處理一樣。如圖1.3所示,獨(dú)立的進(jìn)程可以通過進(jìn)程間常規(guī)的通信渠道傳遞訊息(信號(hào)、套接字、文件、管道等等)。不過,這種進(jìn)程之間的通信通常不是設(shè)置復(fù)雜,就是速度慢,這是因?yàn)椴僮飨到y(tǒng)會(huì)在進(jìn)程間提供了一定的保護(hù)措施,以避免一個(gè)進(jìn)程去修改另一個(gè)進(jìn)程的數(shù)據(jù)。還有一個(gè)缺點(diǎn)是,運(yùn)行多個(gè)進(jìn)程所需的固定開銷:需要時(shí)間啟動(dòng)進(jìn)程,操作系統(tǒng)需要內(nèi)部資源來管理進(jìn)程,等等。
當(dāng)然,以上的機(jī)制也不是一無是處:操作系統(tǒng)在進(jìn)程間提供附加的保護(hù)操作和更高級(jí)別的通信機(jī)制,意味著可以更容易編寫安全的并發(fā)代碼。實(shí)際上,在類似于Erlang的編程環(huán)境中,將進(jìn)程作為并發(fā)的基本構(gòu)造塊。
使用多進(jìn)程實(shí)現(xiàn)并發(fā)還有一個(gè)額外的優(yōu)勢———可以使用遠(yuǎn)程連接(可能需要聯(lián)網(wǎng))的方式,在不同的機(jī)器上運(yùn)行獨(dú)立的進(jìn)程。雖然,這增加了通信成本,但在設(shè)計(jì)精良的系統(tǒng)上,這可能是一個(gè)提高并行可用行和性能的低成本方式。
http://wiki.jikexueyuan.com/project/cplusplus-concurrency-action/images/chapter1/1-3.png" alt="" />
圖 1.3 一對并發(fā)運(yùn)行的進(jìn)程之間的通信
并發(fā)的另一個(gè)途徑,在單個(gè)進(jìn)程中運(yùn)行多個(gè)線程。線程很像輕量級(jí)的進(jìn)程:每個(gè)線程相互獨(dú)立運(yùn)行,且線程可以在不同的指令序列中運(yùn)行。但是,進(jìn)程中的所有線程都共享地址空間,并且所有線程訪問到大部分?jǐn)?shù)據(jù)———全局變量仍然是全局的,指針、對象的引用或數(shù)據(jù)可以在線程之間傳遞。雖然,進(jìn)程之間通常共享內(nèi)存,但是這種共享通常是難以建立和管理的。因?yàn)?,同一?shù)據(jù)的內(nèi)存地址在不同的進(jìn)程中是不相同。圖1.4展示了一個(gè)進(jìn)程中的兩個(gè)線程通過共享內(nèi)存進(jìn)行通信。
http://wiki.jikexueyuan.com/project/cplusplus-concurrency-action/images/chapter1/1-4.png" alt="" />
圖 1.4 同一進(jìn)程中的一對并發(fā)運(yùn)行的線程之間的通信
地址空間共享,以及缺少線程間數(shù)據(jù)的保護(hù),使得操作系統(tǒng)的記錄工作量減小,所以使用多線程相關(guān)的開銷遠(yuǎn)遠(yuǎn)小于使用多個(gè)進(jìn)程。不過,共享內(nèi)存的靈活性是有代價(jià)的:如果數(shù)據(jù)要被多個(gè)線程訪問,那么程序員必須確保每個(gè)線程所訪問到的數(shù)據(jù)是一致的(在本書第3、4、5和8章中會(huì)涉及,線程間數(shù)據(jù)共享可能會(huì)遇到的問題,以及如何使用工具來避免這些問題)。問題并非無解,只要在編寫代碼時(shí)適當(dāng)?shù)刈⒁饧纯?,這同樣也意味著需要對線程通信做大量的工作。
多個(gè)單線程/進(jìn)程間的通信(包含啟動(dòng))要比單一進(jìn)程中的多線程間的通信(包括啟動(dòng))的開銷大,若不考慮共享內(nèi)存可能會(huì)帶來的問題,多線程將會(huì)成為主流語言(包括C++
)更青睞的并發(fā)途徑。此外,C++
標(biāo)準(zhǔn)并未對進(jìn)程間通信提供任何原生支持,所以使用多進(jìn)程的方式實(shí)現(xiàn),這會(huì)依賴與平臺(tái)相關(guān)的API。因此,本書只關(guān)注使用多線程的并發(fā),并且在此之后所提到“并發(fā)”,均假設(shè)為多線程來實(shí)現(xiàn)。
了解并發(fā)后,讓來看看為什么要使用并發(fā)。