《七周七並發模型》第一章概述
並發編程雖不是新的概念,最近卻逐漸熱門起來。一些編程語言,如Erlang、Haskell、Go、Scala、Clojure,也因對並發編程提供了良好的支持,而受到廣泛關注。
並發編程複興的主要驅動力來自於所謂的“多核危機”。正如摩爾定律①所預言的那樣,芯片性能仍在不斷提高,但相比加快CPU的速度,計算機正在向多核化方向②發展。正如Herb Sutter所說,“免費午餐的時代已然終結”③。為了讓代碼運行得更快,單純依靠更快的硬件已無法滿足要求,我們需要利用多核,也就是發掘並行執行的潛力。
1.1 並發?還是並行?
本書的主題是“並發”,那麼又為何涉及了“並行”呢?雖然兩者有所關聯又常被混淆,但並發和並行的含義卻是不同的。
一字之差也是差。一個並發程序含有多個邏輯上的獨立執行塊④,它們可以獨立地並行執行,也可以串行執行。
一個並行程序解決問題的速度往往比一個串行程序快得多,因為其可以同時執行整個任務的多個部分。並行程序可能有多個獨立執行塊,也可能僅有一個。
我們還可以從另一種角度來看待並發和並行之間的差異:並發是問題域中的概念——程序需要被設計成能夠處理多個同時(或者幾乎同時)發生的事件;而並行則是方法域中的概念——通過將問題中的多個部分並行執行,來加速解決問題。
引用Rob Pike的經典描述①:
並發是同一時間應對(dealing with)多件事情的能力;並行是同一時間動手做(doing)多件事情的能力。
那麼這本書講述的是並發還是並行?
我妻子是一位教師。與眾多教師一樣,她極其善於處理多個任務。她雖然每次隻能做一件事,但可以並發地處理多個任務。比如,在聽一位學生朗讀的時候,她可以暫停學生的朗讀,以維持課堂秩序,或者回答學生的問題。這是並發,但並不並行(因為僅有她一個人,某一時刻隻能進行一件事)。
但如果還有一位助教,則她們中一位可以聆聽朗讀,而同時另一位可以回答問題。這種方式既是並發,也是並行。
假設班級設計了自己的賀卡並要批量製作。一種方法是讓每位學生製作五枚賀卡。這種方法是並行,而(從整體看)不是並發,因為這個過程整體來說隻有一個任務。
超越串行編程模型
並發和並行的共同點就是它們比傳統的串行編程模型更優秀。 本書將同時涵蓋並發和並行(學究可能會給這本書起名為“七周七並發模型和並行模型”, 不過那樣的話本書的封麵會變得很難看)。
並發和並行經常被混淆的一個原因是,傳統的“線程與鎖”模型並沒有顯式支持並行。如果要用線程與鎖模型為多核進行開發,唯一的選擇就是寫一個並發的程序,讓其並行地運行在多核上。
然而,並發程序通常是不確定的,它會隨著事件時序的改變而給出不同的結果。對於一個真正的並發程序,不確定性是其與生俱來且伴隨始終的屬性。與之相反,並行程序可能是確定的——例如,要將數組中的每個數都加倍,一種做法是將數組分為兩部分並把它們分別交給一個核處理,這種做法的運行結果是確定的。使用一門直接支持並行的編程語言可以寫出並行程序,而不會引入不入不 確定性。
1.2 並行架構
人們通常認為並行等同於多核,但現代計算機在不同層次上都使用了並行技術。比如說,單 核的運行速度現今仍能每年不斷提升的原因是:單核包含的晶體管數量,如同摩爾定律預測的那 樣變得越來越多,而單核在位級和指令級兩個層次上都能夠並行地使用這些晶體管資源。
位級(bit-level)並行
為什麼32位計算機比8位計算機運行速度更快?因為並行。對於兩個32位數的加法,8位計算 機必須進行多次8位計算,而32位計算機可以一步完成,即並行地處理32位數的4字節。 計算機的發展經曆了8位、16位、32位,現在正處於64位時代。然而由位升級帶來的性能改 善是存在瓶頸的,這也正是短期內我們無法步入128位時代的原因。
指令級(instruction-level)並行
現代CPU的並行度很高,其中使用的技術包括流水線、亂序執行和猜測執行等。
程序員通常可以不關心處理器內部並行的細節,因為盡管處理器內部的並行度很高,但是經 過精心設計,從外部看上去所有處理都像是串行的。
而這種“看上去像串行”的設計逐漸變得不適用。處理器的設計者們為單核提升速度變得越 來越困難。進入多核時代,我們必須麵對的情況是:無論是表麵上還是實質上,指令都不再串行 執行了。我們將在2.2節的“內存可見性”部分展開討論。
數據級(data)並行 數據級並行
(也稱為“單指令多數據”,SIMD)架構,可以並行地在大量數據上施加同一操 作。這並不適合解決所有問題,但在適合的場景卻可以大展身手。
圖像處理就是一種適合進行數據級並行的場景。比如,為了增加圖片亮度就需要增加每一個像 素的亮度。現代GPU(圖形處理器)也因圖像處理的特點而演化成了極其強大的數據並行處理器。
任務級(task-level)並行
終於來到了大家所默認的並行形式——多處理器。從程序員的角度來看,多處理器架構最明 顯的分類特征是其內存模型(共享內存模型或分布式內存模型)。
對於共享內存的多處理器係統,每個處理器都能訪問整個內存,處理器之間的通信主要通過 內存進行,如圖1-1所示。
圖1-1 共享內存的多處理器係統
對於分布式內存的多處理器係統,每個處理器都有自己的內存,處理器之間的通信主要通過 網絡進行,如圖1-2所示。
圖1-2 分布式內存的多處理器係統
通過內存通信比通過網絡通信更簡單更快速,所以用共享內存編程往往更容易。然而,當處 理器個數逐漸增多,共享內存就會遭遇性能瓶頸——此時不得不轉向分布式內存。如果要開發一 個容錯係統,就要使用多台計算機以規避硬件故障對係統的影響,此時也必須借助於分布式內存。
1.3 並發:不隻是多核
使用並發的目的,不僅僅是為了讓程序並行運行從而發揮多核的優勢。若正確使用並發,程序還將獲得以下優點:及時響應、高效、容錯、簡單。
並發的世界,並發的軟件
世界是並發的,為了與其有效地交互,軟件也應是並發的。
一部手機可以同時播放音樂、上網瀏覽、響應觸屏動作。我們在IDE中輸入代碼時,IDE正在後台悄悄檢查代碼語法。一架飛機上的係統也同時兼顧了好幾件事情:監控傳感器、在儀表盤上顯示信息、執行指令、操縱飛行裝置調整飛行姿態。
並發是係統及時響應的關鍵。比如,當文件下載可以在後台進行時,用戶就不必一直盯著鼠標沙漏而煩心了。再比如,Web服務器可以並發地處理多個連接請求,一個慢請求不會影響服務器對其他請求的響應。
分布式的世界,分布式的軟件
有時,我們要解決地理分布型問題。軟件在非同步運行的多台計算機上分布式地運行,其本質是並發。
此外,分布式軟件還具有容錯性。我們可以將服務器一半部署在歐洲,另一半部署在美國,這樣如果一個區域停電就不會造成軟件整體不可用。下麵就介紹容錯性1。
1. 作者在此處用到了兩個詞:”fault-tolerant”和”resilient”,中文都譯為”容錯性”,但兩者略有區別。由於這種微小的區別不會影響對本書的理解,因此之後的譯文不再區分兩者,統一使用”容錯性”以方便讀者理解。——譯者注
不可預測的世界,容錯性強的軟件
軟件有bug,程序會崩潰。即使存在完美的沒有bug的程序,運行程序的硬件也可能出現故障。
為了增強軟件的容錯性,並發代碼的關鍵是獨立性和故障檢測。獨立性是指一個故障不會影響到故障任務以外的其他任務。故障檢測是指當一個任務失敗時(原因可能是任務崩潰、失去響應或硬件故障),需要通知負責故障處理的其他任務來處理。
串行程序的容錯性遠不如並發程序。
複雜的世界,簡單的軟件
如果曾經花費數小時糾結在一個難以診斷的多線程bug上,那你可能很難接受這個結論,但在選對編程語言和工具的情況下,比起串行的等價解決方案,一個並發的解決方案會更簡潔清晰。
在處理現實世界的並發問題時,這個結論可以得到印證。用串行方案解決一個並發問題往往需要付出額外的代價,而且解決方案會晦澀難懂。如果解決方案有著與問題類似的並發結構,就會簡單許多:我們不需要創建一個複雜的線程來處理問題中的多個任務,隻需要用多個簡單的線程分別處理不同的任務即可。
1.4 七個模型
本書精心挑選了七個模型來介紹並發與並行。
線程與鎖:線程與鎖模型有很多眾所周知的不足,但仍是其他模型的技術基礎,也是很多並發軟件開發的首選。
函數式編程:函數式編程日漸重要的原因之一,是其對並發編程和並行編程提供了良好的支持。函數式編程消除了可變狀態,所以從根本上是線程安全的,而且易於並行執行。
Clojure之道——分離標識與狀態:編程語言Clojure是一種指令式編程和函數式編程的混搭方案,在兩種編程方式上取得了微妙的平衡來發揮兩者的優勢。
actor:actor模型是一種適用性很廣的並發編程模型,適用於共享內存模型和分布式內存模型,也適合解決地理分布型問題,能提供強大的容錯性。
通信順序進程(Communicating Sequential Processes,CSP):表麵上看,CSP模型與actor模型很相似,兩者都基於消息傳遞。不過CSP模型側重於傳遞信息的通道,而actor模型側重於通道兩端的實體,使用CSP模型的代碼會帶有明顯不同的風格。
數據級並行:每個筆記本電腦裏都藏著一台超級計算機——GPU。GPU利用了數據級並行,不僅可以快速進行圖像處理,也可以用於更廣闊的領域。如果要進行有限元分析、流體力學計算或其他的大量數字計算,GPU的性能將是不二選擇。
Lambda架構:大數據時代的到來離不開並行——現在我們隻需要增加計算資源,就能具有處理TB級數據的能力。Lambda架構綜合了MapReduce和流式處理的特點,是一種可以處理多種大數據問題的架構。
最後更新:2017-05-23 09:31:39