並發編程的七個模型


線程與鎖:線程與鎖模型有很多眾所周知的不足,但仍是其他模型的技術基礎,也是很多並發軟件開發的首選。

函數式編程:函數式編程日漸重要的原因之一,是其對並發編程和並行編程提供了良好的支持。函數式編程消除了可變狀態,所以從根本上是線程安全的,而且易於並行執行。

Clojure之道——分離標識與狀態:編程語言Clojure是一種指令式編程和函數式編程的混搭方案,在兩種編程方式上取得了微妙的平衡來發揮兩者的優勢。

actor:actor模型是一種適用性很廣的並發編程模型,適用於共享內存模型和分布式內存模型,也適合解決地理分布型問題,能提供強大的容錯性。

通信順序進程(Communicating Sequential Processes,CSP):表面上看,CSP模型與actor模型很相似,兩者都基於消息傳遞。不過CSP模型側重於傳遞信息的通道,而actor模型側重於通道兩端的實體,使用CSP模型的代碼會帶有明顯不同的風格。

數據級並行:每個筆記本電腦里都藏着一台超級計算機——GPU。GPU利用了數據級並行,不僅可以快速進行圖像處理,也可以用於更廣闊的領域。如果要進行有限元分析、流體力學計算或其他的大量數字計算,GPU的性能將是不二選擇。

Lambda架構:大數據時代的到來離不開並行——現在我們只需要增加計算資源,就能具有處理TB級數據的能力。Lambda架構綜合了MapReduce和流式處理的特點,是一種可以處理多種大數據問題的架構。

1. 線程與鎖

原始,底層(既是優點也是缺點),有效,仍然是開發並發軟件的首選。 
幾乎每種編程語言都以某種形式提供了支持,我們應該了解底層的原理,但是,多數時候,應該使用更上層的類庫,更高效,更不易出錯。 
這種方式無外乎幾種經典的模式,互斥鎖(臨界區),生產者-消費者,同步等等。 
書中舉了個外星方法的示例,即使我們自己的代碼沒有死鎖,但是你不知道調用的方法做了什么,或許就會導致死鎖。 
Java提供了一些類庫來解決並發編程的問題。 
比如ReentrantLock 
a. 可中斷,如果是原始的Thread,除了停止JVM,沒有其他方式終止死鎖; 
b. 超時,需要的資源到時間沒有釋放,不會阻塞在那里,我們也可以在后續把之前的資源釋放掉,反正當前任務也不能執行,不如提高系統整體的能力; 
c. 交替鎖,比如插入鏈表時,不用鎖整個鏈表,只要鎖兩個相關的結點即可 
d. 條件變量,當滿足某種條件時,才繼續執行 
比如atomic包 
原子操作。不會忘了在正確的時間獲取鎖;由於沒有鎖,不會死鎖;非阻塞。 
線程池,大小很重要,對於CPU密集型,大小是CPU可用核數,對於IO密集型,可以適當大些,比如*2。更好的方式是做壓力測試以衡量性能。 
多線程程序難點不在於難以編寫,而是在於難以測試。 
找到bug原因也很難,有點程序一直運行良好,但是過幾個月才出一次問題,絕對很難定位問題。 
這種模型也是其他某些模型的基礎。

2. 函數式編程

對於線程間共享的可變的數據可能會出現各種問題,我們另辟蹊徑,對於不變的數據,多線程不用鎖就可以安全地進行訪問。這就是為什么函數式編程會如此的引人入勝,它沒有可變狀態,所以不會遇到共享可變狀態帶來的種種問題。 
傳統的程序可能會隱藏可變的狀態或者存在逃逸的可變狀態。比如Java里面的DateFormat.parse就會隱匿可變的狀態,多線程使用的時候就有可能拋出異常,我在雲知聲實習的時候就遇到這個問題;再比如兩個函數都加了同步關鍵字,一個返回迭代器,一個往List里面add,也會出問題的。 
而函數式編程處理並行的時候,就不會有這些問題。 
除了並行,函數式並發也值得關注。因為函數式語言中的函數具有引用透明性,在任何調用函數的地方,都可以用函數運行的結果來替換函數的調用,而不會對程序產生副作用。比如:(+ (+ 1 2) (+ 3 4)),先計算1+2或者先計算3+4,對結果是沒有影響的,也就是說,同樣的結構,不同的求職順序,結果一致。Clojure提供了future和promise模型,就是用類似的思想來解決並發問題的。 
很多人認為並行一定會伴隨着不確定性,這是不對的。對於有時序依賴的問題,會有不確定性,但是對於從0加到10000,或者統計某個頁面的詞頻,結果應該總是一樣的。 
使用線程和鎖的模型中,大多數潛在的競態條件並不是來源於問題本身的不確定性,而是隱藏於解決方案的細節中。

3. 標識和狀態分離

如果一個線程引用了持久數據結構,那么其他線程對數據結構的修改對該線程就是不可見的。因此持久數據結構對並發編程的意義非比尋常,其分離了標識(identity)與狀態(state)。

“你的汽車有多少油”是一個標識,其狀態是一直在改變的,也就是說,實際上它是一系列不同的值——2012-02-23 12:03,值是0.53;2012-02-23 14:30,值是0.12;2012-02-23 14:31,值是1.00。

命令式語言中,一個變量混合了標識與狀態——一個標識只能擁有一個值,這讓我們很容易忽略一個事實:狀態實際上是隨時間變化的一系列值。持久數據結構將標識與狀態分離開來——如果獲取了一個標識的當前狀態,無論將來對這個標識怎樣修改,獲取的那個狀態將不再改變。Clojure是一門不純粹的函數式語言,提供了大量的可變數據類型。我們已經學習了其中最簡單的一種——原子變量。命令式語言和不純粹的函數式語言的區別是今天的一個重點。命令式語言中,變量默認是狀態易變的,代碼會經常修改變量。不純粹的函數式語言中,變量默認是狀態不易變的,代碼僅在必要時修改變量。函數式語言中,數據結構是持久的,也就是說當一個線程修改它時,將不會影響到引用同一個數據結構的其他線程。借助上述特性,我們可以分離標識與狀態。與標識不同,狀態實際上是一系列隨時間變化的值。

STM事務具有原子性、一致性和隔離性。

  • 原子性:在其他的事務看來,當前事務的所有副作用或者全部發生,或者都不發生。

  • 一致性:事務保證全程遵守校驗器定義的規范(就像我們在原子變量和代理中看到的一樣)。如果事務的一系列修改中任一個校驗失敗,那么所有的修改都不會發生。

  • 隔離性:多個事務可以同時運行,但同時運行的事務的結果與串行運行這些事務的結果應當完全一樣。

你可能已經看出來了,這三個性質是許多數據庫支持的ACID特性中的前三個。唯一遺漏的性質是持久性——STM的數據在電源故障或系統崩潰時會丟失。如果需要用到持久性,就必須使用數據庫。

原子變量可以對單一值進行隔離的、同步的更新。 
代理可以對單一值進行隔離的、異步的更新。 
引用可以對多個值進行一致的、同步的更新。

我們可以用Clojure“函數式地”解決函數式的問題,也可以在必要的時候突破函數式的禁錮。

傳統命令式語言的變量混淆了標識與狀態這兩個概念,而Clojure的持久數據結構將可變量的標識與狀態分離開來。這解決了使用鎖的方案的大部分缺點。專家級Clojure程序員知道解決並發問題的最佳選擇是那個“剛剛夠用”的方案。 
“Clojure之道”的主要缺點在於不支持分布式(地理分布或其他)編程。與之相關,它也無法直接提供容錯性。 
由於Clojure在JVM中運行,很多第三方庫可以為Clojure彌補這些缺

4. Actor

多個actor(進程)可以同時運行、不共享狀態、通過向信箱異步地發送消息來進行通信。 
使用actor模型的程序並不進行防御式編程,而是遵循“任其崩潰”的哲學,讓actor的管理者來處理這些問題。這樣做有幾個好處,比如: 
代碼會變得更加簡潔且容易理解,可以清晰區分出“一帆風順”的代碼和容錯代碼; 
多個actor之間是相互獨立的,並不共享狀態,因此一個actor的崩潰不太會殃及到其他actor。尤其重要的是一個actor的崩潰不會影響到其管理者,這樣管理者才能正確處理此次崩潰; 
管理者也可以選擇不處理崩潰,而是記錄崩潰的原因,這樣我們就會得到崩潰通知並進行后續處理。

雖然第一眼看上去“任其崩潰”的哲學有點奇怪,但它和錯誤處理內核模式都在產品環境上反復進行過驗證。一些系統的可用性據說提高到了99.9999999%(9個9)

Smalltalk的設計者、面向對象編程之父Alan Kay曾經這樣描述面向對象的本質:

很久以前,我在描述“面向對象編程”時使用了“對象”這個概念。很抱歉這個概念讓許多人誤入歧途,他們將學習的重心放在了“對象”這個次要的方面。 
真正主要的方面是“消息”……日文中有一個詞ma,表示“間隔”,與其最為相近的英文或許是“ interstitial”。
創建一個規模宏大且可生長的系統的關鍵在於其模塊之間應該如何交流,而不在於其內部的屬性和行為應該如何表現。

 這么多年,看到了對於面向對象不一樣的理解,對象是第二等的,對象之間的交流才是第一位的

actor模型精心設計了消息傳輸和封裝的機制,由此帶來好處,雖然多個actor可以同時運行,但它們並不共享狀態,而且在單個actor中所有事件都是串行執行的。所以關於並發,只需要關注於多個actor之間的消息流即可。每個actor可以被單獨測試,而且當測試覆蓋了某個actor的消息類型和消息順序時,就可以確定這個actor非常可靠。如果發現了一個與並發相關的bug,也就知道重點應該放在actor之間的消息流上。 
容錯 
使用actor模型的程序天生具有容錯性。這不僅會讓程序更加強壯,而且(通過“任其崩潰”的哲學)會讓代碼更加簡潔明了。 


分布式編程 
actor模型支持共享內存模型,也支持分布式內存模型,這就帶來了很多優點。首先,actor模型幾乎可以解決任何規模的問題。我們不需要將問題局限於用一個系統解決。其次,actor模型可以解決地理分布式問題。對於不同部分需要部署在不同地理位置的軟件,Actor模型是個極佳的選擇。最后,分布式是軟件具有容錯能力的基石。 

缺點
缺點也是有的,而且有該模型固有的某些缺點: 
actor模型的程序比使用線程與鎖模型的程序更容易debug,但actor模型仍會碰到死鎖這一類的共性問題,也會碰到一些actor模型獨有的問題(例如信箱溢出)。類似於線程與鎖模型,actor模型對並行也沒有提供直接支持。需要通過並發的技術來構造並行的方案,這樣就會引入不確定性。而且,由於多個actor並不共享狀態,僅通過消息傳遞來進行交流,所以不太適合實施細粒度的並行。

5. 通信順序模型

通信順序進程(Communicating Sequential Processe,CSP)模型也是由獨立的、並發執行的實體所組成,實體之間也是通過發送消息進行通信。但兩種模型的重要差別是:CSP模型不關注發送消息的實體,而是關注發送消息時使用的channel(通道)。channel是第一類對象,它不像進程那樣與信箱是緊耦合的,而是可以單獨創建和讀寫,並在進程之間傳遞。 
緩沖區往往有三種類型:阻塞型,棄用新值型,移出舊值型。 
后面還介紹了異步編程模式,C#中也引入了這種模式。

6. 數據並行

電腦中的超級計算機——圖形處理單元(GPU)。現代GPU是一個強力的數據並行處理器,其用於數學計算時性能超過了CPU,這種做法稱為基於圖形處理器的通用計算(General-Purpose computing on the GPU),或GPGPU編程。 
作者通過OpenCL來講解這種並行模型。 
GPU會綜合流水線、多ALU等技術提高性能,不同的GPU之間差異很大,好在有OpenCL,針對多種架構抽象地進行編程。不同的GPU廠商會提供各自的編譯器和驅動程序,使代碼可以被編譯並在相應的硬件上運行。 
讓人意想不到的是,OpenCL還適用於CPU。 
有其他語言的庫,比如書中第三天就使用了Java庫,其封裝了OpenCL和OpenGL,用起來更方便。 
數據並行非常適用於處理大量數值數據,尤其適合於科學計算、工程計算以及仿真領域,比如流體力學、有限元分析、N體模擬、模擬退火、蟻群優化、神經網絡等。 
它的缺點也就是它的優點太突出:數據並行編程,更准確地說是GPGPU編程,在其適用的領域內所向披靡。但它並不適用於所有問題領域,適用范圍很小。

7. Lambda架構

與GPGPU編程不同,Lambda架構是站在大規模場景的角度來解決問題的,它可以將數據和計算分布到幾十台或幾百台機器構成的集群上進行。這種技術不但解決了之前因為規模龐大而無法解決的難題,還可以構建出對硬件錯誤和人為錯誤進行容錯的系統。 
將一個問題拆分成一個映射操作和一個化簡操作,使其更容易被並行化。MapReduce,在本章中使用的這個術語特指一個使用多台計算機的、由映射操作和化簡操作構成的、高效且容錯的分布式系統。

信息可以分為原始數據和衍生信息。原始數據是永恆的真相,而且是不變的。基於這個特性,利用Lambda架構的批處理層,可以創建具有以下特性的系統:高度並行化,可以處理TB級別的數據;簡單,容易創建且不易出錯;對技術性故障和人為故障進行容錯;支持對日常數據的操作,也支持對歷史數據生成報表和進行分析。

批處理層最大的缺點在於其有延遲,Lambda架構利用加速層來解決這一問題。 
加速層創建的實時視圖包含了最后一次生成批處理視圖后產生的數據,這樣就完善了Lambda架構。

Lambda架構主要用於解決大規模數據的問題——這些問題是傳統數據處理架構難以應對的。Lambda架構非常適合於報表和分析——以前我們會使用數據倉庫來進行這類工作。Lambda架構最大的優點——擅長處理大規模數據——這也正是它的缺點。除非你的數據達到數太字節甚至更多,否則其成本(計算成本和智力成本)將高於收益。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM