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