按最簡單、最基本的程度理解,并發(concurrency)是兩個或多個同時獨立進行的活動。并發現象遍布日常生活,我們可以邊走路邊說話,左右手同時做出不一樣的動作,諸如此類。
計算機系統中的并發
若我們談及計算機系統中的并發,則是指同一個系統中,多個獨立活動同時進行,而非依次進行。
多年來,多任務操作系統可以憑借任務切換,讓同一臺計算機同時運行多個應用軟件,這早已稀松平常,而高端服務器配備了多處理器,實現了“真并發”(genuine concurrency)。
大勢所趨,主流計算機現已能夠真真正正地并行處理多任務,而不再只是制造并發的表象。
很久之前,大多計算機都僅有一個處理器,處理器內只有單一處理單元或單個內核,許多臺式計算機至今依舊如此。這種計算機在同一時刻實質上只能處理一個任務,不過,每秒內,它可以在各個任務之間多次切換,先處理某任務的一小部分,接著切換任務,同樣只處理一小部分,然后對其他任務如法炮制。
看起來所有任務都正在同時執行,因此其被稱為任務切換。至此,我們談及的并發都基于這種模式。由于任務飛速切換,我們難以分辨處理器到底在哪一刻暫停某個任務而切換到另一個。任務切換對使用者和應用軟件自身都制造出并發的表象。
由于是表象,因此對比真正的并發環境,當應用程序在進行任務切換的單一處理器環境下運行時,其行為可能稍微不同。具體而言,如果就內存模型做出不當假設,本來會導致某些問題,但這些問題在上述環境中卻有可能不會出現。
多年來,配備了多處理器的計算機一直被用作服務器,它要承擔高性能的計算任務;現今,基于一芯多核處理器(簡稱多核處理器)的計算機日漸普及,多核處理器也用在臺式計算機上。
無論是裝配多個處理器,還是單個多核處理器,或是多個多核處理器,這些計算機都能真正并行運作多個任務,我們稱之為硬件并發(hardware concurrency)。
圖 1 所示為理想化的情景:

圖 1 兩種并發方式:雙核機上的并發執行與單核機上的任務切換
計算機有兩個任務要處理,將它們進行十等分。在雙核機(具有兩個處理核)上,兩個任務在各自的核上分別執行。另一臺單核機則切換任務,交替執行任務小段,但任務小段之間略有間隔。
圖 1 中,單核機的任務小段被灰色小條隔開,它們比雙核機的分隔條粗大。為了交替執行,每當系統從某一個任務切換到另一個時,就必須完成一次上下文切換(context switch),于是耗費了時間。若要完成一次上下文切換,則操作系統需保存當前任務的 CPU 狀態和指令指針,判定需要切換到哪個任務,并為之重新加載 CPU 狀態。接著,CPU 有可能需要將新任務的指令和數據從內存加載到緩存,這或許會妨礙 CPU,令其無法執行任何指令,加劇延遲。
盡管多處理器或多核系統明顯更適合硬件并發,不過有些處理器也能在單核上執行多線程。真正需要注意的關鍵因素是硬件支持的線程數(hardware threads),也就是硬件自身真正支持同時運行的獨立任務的數量。
即便是真正支持硬件并發的系統,任務的數量往往容易超過硬件本身可以并行處理的數量,因而在這種情形下任務切換依然有用。譬如,常見的臺式計算機能夠同時運行數百個任務,在后臺進行各種操作,表面上卻處于空閑狀態。正是由于任務切換,后臺任務才得以運作,才容許我們運行許多應用軟件,如文字處理軟件、編譯器、編輯軟件,以及瀏覽器等。

圖 2 展示了雙核機上 4 個任務的相互切換,這同樣是理想化的情形,各個任務都被均勻切分。實踐中,許多問題會導致任務切分不均勻或調度不規則。
并發的方式
設想兩位開發者要共同開發一個軟件項目。假設他們處于兩間獨立的辦公室,而且各有一份參考手冊,則他們可以靜心工作,不會彼此干擾。但這令交流頗費周章:他們無法一轉身就與對方交談,遂不得不借助電話或郵件,或是需起身離座走到對方辦公室。另外,使用兩間辦公室有額外開支,還需購買多份參考手冊。
現在,如果安排兩位開發者共處一室,他們就能暢談軟件項目的設計,也便于在紙上或壁板上作圖,從而有助于交流設計的創意和理念。這樣,僅有一間辦公室要管理,并且各種資源通常只需一份就足夠了。但缺點是,他們恐怕難以集中精神,共享資源也可能出現問題。
這兩種安排開發者的辦法示意了并發的兩種基本方式:
- 一位開發者代表一個線程,一間辦公室代表一個進程。第一種方式采用多個進程,各進程都只含單一線程,情況類似于每位開發者都有自己的辦公室;
- 第二種方式只運行單一進程,內含多個線程,正如兩位開發者同處一間辦公室。
我們可以隨意組合這兩種方式,掌控多個進程,其中有些進程包含多線程,有些進程只包含單一線程,但基本原理相同。接著,我們來簡略看看應用軟件中的這兩種并發方式。
多進程并發
在應用軟件內部,一種并發方式是,將一個應用軟件拆分成多個獨立進程同時運行,它們都只含單一線程,非常類似于同時運行瀏覽器和文字處理軟件。這些獨立進程可以通過所有常規的進程間通信途徑相互傳遞信息(信號、套接字、文件、管道等),如圖 3 所示。

這種進程間通信普遍存在短處:或設置復雜,或速度慢,甚至二者兼有,因為操作系統往往要在進程之間提供大量防護措施,以免某進程意外改動另一個進程的數據;還有一個短處是運行多個進程的固定開銷大,進程的啟動花費時間,操作系統必須調配內部資源來管控進程,等等。
進程間通信并非一無是處:通常,操作系統在進程間提供額外保護和高級通信機制。這就意味著,比起線程,采用進程更容易編寫出安全的并發代碼。某些編程環境以進程作為基本構建單元,其并發效果確實一流,譬如為 Erlang 編程語言準備的環境。
運用獨立的進程實現并發,還有一個額外優勢——通過網絡連接,獨立的進程能夠在不同的計算機上運行。這樣做雖然增加了通信開銷,可是只要系統設計精良,此法足以低廉而有效地增強并發力度,改進性能。
多線程并發
另一種并發方式是在單一進程內運行多線程。線程非常像輕量級進程,每個線程都獨立運行,并能各自執行不同的指令序列。
不過,同一進程內的所有線程都共用相同的地址空間,且所有線程都能直接訪問大部分數據。全局變量依然全局可見,指向對象或數據的指針和引用能在線程間傳遞。
盡管進程間共享內存通常可行,但這種做法設置復雜,往往難以駕馭,原因是同一數據的地址在不同進程中不一定相同。圖 4 展示了單一進程內的兩個線程借共享內存通信。

圖 4 單一進程內的兩個線程借共享內存通信
我們可以啟用多個單線程的進程并在進程間通信,也可以在單一進程內發動多個線程而在線程間通信,后者的額外開銷更低。因此,即使共享內存帶來隱患,主流語言大都青睞以多線程的方式實現并發功能。
提到多線程代碼,還常常用到一個詞——并行。接下來,我們來厘清并發與并行的區別。
并發與并行
就多線程代碼而言,并發與并行(parallel)的含義很大程度上相互重疊。確實,在多數人看來,它們就是相同的。
并發和并行的差別甚小,主要是著眼點和使用意圖不同。兩個術語都是指使用可調配的硬件資源同時運行多個任務,但并行更強調性能。當人們談及并行時,主要關心的是利用可調配的硬件資源提升大規模數據處理的性能;當談及并發時,主要關心的是分離關注點或響應能力。
為分離關注點而并發
一直以來,編寫軟件時,分離關注點(separation of concerns)幾乎總是不錯的構思:歸類相關代碼,隔離無關代碼,使程序更易于理解和測試,因此所含缺陷很可能更少。
并發技術可以用于隔離不同領域的操作,即便這些不同領域的操作需同時進行;若不直接使用并發技術,我們將不得不編寫框架做任務切換,或者不得不在某個操作步驟中,頻繁調用無關領域的代碼。
考慮一個帶有用戶界面的應用軟件,需要由 CPU 密集處理,如臺式計算機上的 DVD 播放軟件。本質上,這個應用軟件肩負兩大職責:既要從碟片讀取數據,解碼聲音影像,并將其及時傳送給圖形硬件和音效硬件,讓 DVD 順暢放映,又要接收用戶的操作輸入,譬如用戶按“暫?!?、“返回選項單”、“退出”等鍵。假若采取單一線程,則該應用軟件在播放過程中,不得不定時檢查用戶輸入,結果會混雜播放 DVD 的代碼與用戶界面的代碼。
改用多線程就可以分離上述兩個關注點,一個線程只負責用戶界面管理,另一個線程只負責播放 DVD,用戶界面的代碼和播放 DVD 的代碼遂可避免緊密糾纏。兩個線程之間還會保留必要的交互,例如按“暫?!辨I,不過這些交互僅僅與需要立即處理的事件直接關聯。
如果用戶發送了操作請求,而播放 DVD 線程正忙,無法馬上處理,那么在請求被傳送到該線程的同時,代碼通常能令用戶界面線程立刻做出響應,即便只是顯示光標或提示“請稍候”。這種方法使得應用軟件看起來響應及時。類似地,某些必須在后臺持續工作的任務,則常常交由獨立線程負責運行,例如,讓桌面搜索應用軟件監控文件系統變動。此法基本能大幅簡化各線程的內部邏輯,原因是線程間交互得以限定于代碼中可明確辨識的切入點,而無須將不同任務的邏輯交錯散置。
這樣,線程的實際數量便與 CPU 既有的內核數量無關,因為用線程分離關注點的依據是設計理念,不以增加運算吞吐量為目的。
為性能而并行:任務并行和數據并行
多處理器系統已存在數十年,不過一直以來它們大都只見于巨型計算機、大型計算機和大型服務器系統。但是,芯片廠家日益傾向設計多核芯片,在單一芯片上集成 2 個、4 個、16 個或更多處理器,從而使其性能優于單核芯片。于是,多核臺式計算機日漸流行,甚至多核嵌入式設備亦然。
不斷增強的算力并非得益于單個任務的加速運行,而是來自多任務并行運作。從前,處理器更新換代,程序自然而然隨之加速,程序員可以“坐享其成,不勞而獲”。但現在,正如 Herb Sutter 指出的“免費午餐沒有了!”,軟件若要利用增強的這部分算力,就必須設計成并發運行任務。所以程序員必須警覺,特別是那些躊躇不前、忽視并發技術的同業,有必要注意熟練掌握并發技術,儲備技能。
增強性能的并行方式有兩種,分別是任務并行和數據并行。
第一種,最直觀地,將單一任務分解成多個部分,各自并行運作,從而節省總運行耗時。此方式即為任務并行。盡管聽起來淺白、直接,但這卻有可能涉及相當復雜的處理過程,因為任務各部分之間也許存在紛繁的依賴。任務分解可以針對處理過程,調度某線程運行同一算法的某部分,另一線程則運行其他部分;也可以針對數據,線程分別對數據的不同部分執行同樣的操作,這被稱為數據并行。
易于采用上述并行方式的算法常常被稱為尷尬并行算法。其含義是,將算法的代碼并行化實在簡單,甚至簡單得會讓我們尷尬,實際上這是好事。我還遇見過用其他術語描述這類算法,叫“天然并行”(naturally parallel)與“方便并發”(conveniently concurrent)。尷尬并行算法具備的優良特性是可按規模伸縮——只要硬件支持的線程數目增加,算法的并行程度就能相應提升。這種算法是成語“眾擎易舉”的完美體現。算法中除尷尬并行以外的部分,可以另外劃分成一類,其并行任務的數目固定(所以不可按規模伸縮)。
第二種增強性能的并行方式是利用并行資源解決規模更大的問題。
例如,只要條件適合,便同時處理 2 個文件,或者 10 個,甚至 20 個,而不是每次 1 個。同時對多組數據執行一樣的操作,實際上是采用了數據并行,其著眼點有別于任務并行。采用這種方式處理單一數據所需的時間依舊不變,而同等時間內能處理的數據相對更多。這種方式明顯存在局限,雖然并非任何情形都會因此受益,但數據吞吐量卻有所增加,進而帶來突破。例如,若能并行處理視頻影像中不同的區域,就會提升視頻處理的解析度。

