爲何現代系統須要新的編程模型Akka

Akka中最重要的即是actor模型。html

幾十年前,Carl Hewitt提出了actor模型,以做爲在高性能網絡下並行處理的一種方式。可是在當時並無這樣的環境,現在硬件和基礎設施能力已經遇上並超越了Carl Hewitt當時的預料。因此,那些想構建高性能分佈式系統的組織遇到的使用面向對象編程(OOP)模型沒法徹底解決的挑戰,如今可使用actor模型解決。編程

現在,actor模型不只被認爲是一種高效的解決方案,並且也已經在世界上一些要求苛刻的應用中獲得了驗證。爲了突出actor模型所能解決的問題,本主題主要討論傳統編程思想與現代多線程多CPU架構之間的不匹配:後端

  • 封裝的挑戰
  • 共享內存的錯覺
  • 調用棧的錯覺

封裝的挑戰

OOP的核心是封裝。封裝規定對象內的數據不能直接從外部訪問,只能調用方法來進行修改。對象須要暴露出一些安全的操做,這些安全操做用來維護其內部數據的約束。緩存

例如,對有序二叉樹的操做不得違反二叉樹有序的約束。調用者但願排序是完整的,當查詢樹中某個數據時,他們須要可以依賴這個約束。安全

當咱們分析OOP運行時行爲時,咱們有時會繪製一個時序圖,顯示方法調用的交互。微信

image.png

不幸的是,上圖並不能準確地表示執行期間實例的生命週期。實際上,全部這些調用發生在同一線程上。網絡

image.png

當您嘗試模擬多線程狀況時,上面的這種表達方式就變得更加清晰了。由於咱們能夠經過下圖來表示兩個線程訪問同一個實例:多線程

image.png

兩個線程進入同一個對象相同的方法,可是對象的封裝模型並不能很好的表達這其中發生的事情。兩個線程能夠以任意方式交錯,想象一下,若是是多線程,這個問題會更加嚴重。架構

解決此問題的經常使用方法是給這些方法加鎖。雖然這確保了在任何給定時間最多隻有一個線程將進入該方法,但這是一種很是昂貴的策略:併發

  • 鎖嚴重限制了併發性,它們在現代CPU架構上很是昂貴,須要操做系統暫停線程而且在以後還要恢復它。
  • 調用者線程被阻塞後沒法執行任何其餘有意義的工做。即便在桌面應用程序中這也是不可接受的,咱們但願即便在後臺有耗時比較久的做業運行時,也要保持面向用戶的應用程序部分可以響應用戶的請求。在後端,阻塞是徹頭徹尾的浪費。有人可能認爲這能夠經過啓動新線程來補償,但線程的代價也很是高昂。
  • 使用鎖還可能致使死鎖。

這些現實致使了一種尷尬的局面:

  • 若是沒有足夠的鎖,對象的正常狀態會被破壞
  • 若是使用不少鎖,性能會受到影響而且很容易致使死鎖。

此外,鎖只能在本地很好地工做。在協調跨多臺機器時,惟一的選擇是分佈式鎖。不幸的是,分佈式鎖的效率比本地鎖效率要差幾個級別,而且在擴展時有更多的限制。分佈式鎖須要在多臺計算機上經過網絡進行屢次通訊往返,所以還存在延遲。

在面嚮對象語言中,咱們不多考慮線程的執行路徑。咱們常常將系統設想爲一個由對象組織成的網絡,它們對方法調用做出反應,並修改其內部狀態,而後經過方法調用相互通訊,從而驅動整個應用程序運行。

image.png

可是,在多線程分佈式環境中,其實是線程經過方法調用「遍歷」此對象網絡。所以,真正推進應用程序運行的是線程:

image.png

總結:

  • 對象封裝只能保證單線程訪問時的對象內部狀態的安全,多線程執行時幾乎總會致使內部狀態的損壞。
  • 雖然鎖彷佛是支持多線程環境下的補救措施,但實際上它們效率低,而且容易致使死鎖。
  • 鎖更適合在本地工做。

共享內存的錯覺

在80-90年代的編程概念模型中,本地變量是直接寫入到內存中的(這和咱們理解的本地變量是存在寄存器中是不同的)。在現代架構上,CPU是寫入到緩存行而不是直接寫入內存的。這些高速緩存大多數都在CPU內核中,也就是說,一個內核的寫入不會被另外一個內核看到。爲了使內核中的本地更改對另外一個核心可見,須要將緩存行傳送到另外一個核心中。

在JVM中,咱們必須使用volatile標記或使用Atomic包裝類明確表示變量要跨線程進行內存共享。不然,咱們只能先加鎖而後訪問它們。爲何咱們不將全部變量都標記爲volatile?由於跨核心同步緩存行是一項很是昂貴的操做!這樣作會阻止內核去執行額外的工做,並致使緩存一致性協議出現瓶頸。

即便對於瞭解這種狀況的開發人員來講,肯定哪些變量應該被標記爲volatile,或者使用哪一種atomic結構也是一種藝術。

總結:

  • 沒有真正的共享內存,CPU核心就像網絡上的計算機同樣須要將數據塊(高速緩存行)同步給其餘CPU核心。CPU間通訊和網絡通訊是類似的。
  • 經過將變量標記爲volatile或使用Atomic結構來使CPU核心之間的數據進行同步是能夠被替代的,咱們可使用一種更有紀律和原則性的方式,將本地變量保存到併發實體內,而後經過消息顯式地在併發實體之間傳播數據或事件。

調用棧的錯覺

今天,咱們將調用棧視爲理所固然。可是,它們是在一個併發編程並不重要的時代發明的,由於那時多CPU系統並不常見,調用棧不會跨線程。

當主線程打算將任務委託給「後臺」時,這實際上就是將任務委託給另一個工做線程,實際上就是主線程將一個任務對象放入工做線程中的一個共享隊列裏,工做線程負責從這個隊列裏獲取任務來執行,這就容許主線程繼續前進並執行其餘任務。

他的第一個問題是,工做線程如何通知主線程任務已完成?當任務因異常而失敗時會出現更嚴重的問題,異常傳播到哪裏?真實狀況是它將傳播到工做線程的異常處理程序,而後徹底忽略了實際的「調用者」是主線程:

image.png

這是一個嚴重的問題。主線程的調用棧上不能捕獲這個異常,工做線程如何處理這種狀況?須要以某種方式通知主線程,例如將異常放在主線程預先準備存放結果的地方,但若是主線程一直沒有收到通知,任務也即丟失!

當工做線程在執行任務時出現BUG,致使工做線程關閉,這時誰來從新啓動一個線程來處理這個任務,而且該任務如何恢復到正常的狀態。這都是問題。

總結:

  • 爲了在當前系統上實現有意義的高性能併發,線程必須以有效的方式在彼此之間委派任務而且不會發生阻塞。使用這種任務委託方式,基於調用棧的錯誤處理會中斷,而且須要引入新的、明確的錯誤通知機制。失敗處理成爲併發系統中要考慮的一部分。
  • 併發系統須要處理服務故障並須要具備從中恢復的方法。此類服務的客戶端須要知道任務/消息可能在從新啓動期間丟失。即便沒有發生丟失,因爲先前排隊的任務比較多,垃圾收集形成的延遲等,響應可能會被延遲。面對這些,併發系統應該以超時的形式處理響應,就像網絡/分佈式系統同樣。

原文爲akka官網連接:doc.akka.io/docs/akka/c…

接下來的文章,讓咱們看看actor模型如何克服這些挑戰。

若是以爲這篇文章能讓你學到知識,可否幫忙轉發,將知識分享出去。 若是想第一時間學習更多的精彩的內容,請關注微信公衆號:1點25

相關文章
相關標籤/搜索