Akka系列(六):Actor解決了什麼問題?

這段時間因爲忙畢業前先後後的事情,拖更了好久,表示很是抱歉,迴歸後的第一篇文章主要是看到了Akka最新文檔中寫的What problems does the actor model solve?,閱讀完後以爲仍是蠻不錯,能簡潔清晰的闡述目前併發領域遇到的問題,併爲什麼利用Actor模型能夠解決這些問題,本文主要是利用本身的理解將這篇文章進行翻譯,有不足之處還請指出。html

Actor解決了什麼問題?

Akka使用Actor模型來克服傳統面向對象編程模型的侷限性,並應對高併發分佈式系統所帶來的挑戰。 充分理解Actor模型是必需的,它有助於咱們認識到傳統的編程方法在併發和分佈式計算的領域上的不足之處。編程

封裝的弊端

面向對象編程(OOP)是一種普遍採用的,熟悉的編程模型,它的一個核心理念就是封裝,並規定對象封裝的內部數據不能從外部直接訪問,只容許相關的屬性方法進行數據操做,好比咱們熟悉的Javabean中的getX,setX等方法,對象爲封裝的內部數據提供安全的數據操做。緩存

舉個例子,有序二叉樹必須保證樹節點數據的分佈規則,若你想利用有序二叉樹進行查詢相關數據,就必需要依賴這個約束。安全

當咱們在分析面向對象編程在運行時的行爲時,咱們可能會繪製一個消息序列圖,用來顯示方法調用時的交互,以下圖所示:網絡

seq chart
seq chart

但上述圖表並不能準確地表示實例在執行過程當中的生命線。實際上,一個線程執行全部這些調用,而且變量的操做也在調用該方法的同一線程上。爲剛纔的序列圖加上執行線程,看起來像這樣:數據結構

seq chart thread
seq chart thread

但當在面對多線程的狀況下,會發現此前的圖愈來愈混亂和變得不清晰,如今咱們模擬多個線程訪問同一個示例:多線程

seq chart multi thread
seq chart multi thread

在上面的這種狀況中,兩個線程調用同一個方法,但別調用的對象並不能保證其封裝的數據發生了什麼,兩個調用的方法指令能夠任意方式的交織,沒法保證共享變量的一致性,如今,想象一下在更多線程下這個問題會更加嚴重。架構

解決這個問題最一般的方法就是在該方法上加鎖。經過加鎖能夠保證同一時刻只有一個線程能進入該方法,但這是一個代價很是昂貴的方法:併發

  • 鎖很是嚴重的限制併發,它在如今的CPU架構上代價是很是大的,它須要操做系統暫停和重啓線程。異步

  • 調用者的線程會被阻塞,以至於它不能去作其餘有意義的任務,舉個例子咱們但願桌面程序在後臺運行的時候,操做UI界面也能獲得響應。在後臺,,線程阻塞徹底是浪費的,有人可能會說能夠經過啓動新線程進行補償,但線程也是一種很是昂貴的資源。

  • 使用鎖會致使一個新的問題:死鎖。

這些現實存在的問題讓咱們只能二者選一:

  • 不使用鎖,但會致使狀態混亂。

  • 使用大量的鎖,可是會下降性能並很容易致使死鎖。

另外,鎖只能在本地更好的利用,當咱們的程序部署在不一樣的機器上時,咱們只能選擇使用分佈式鎖,但不幸的是,分佈式鎖的效率可能比本地鎖低好幾個量級,對後續的擴展也會有很大的限制,分佈式鎖的協議要求多臺機器在網絡上進行相互通訊,所以延遲可能會變得很是高。

在面嚮對象語言中,咱們不多會去考慮線程或者它的執行路徑,咱們一般將系統想象成許多實例對象鏈接成的網絡,經過方法調用,修改實例對象內部的狀態,而後經過實例對象以前的方法調用驅動整個程序進行交互:

object graph
object graph

而後,在多線程分佈式環境中,實際上線程是經過方法調用遍歷這個對象實例網絡。所以,線程是方法調用驅動執行的:

object graph snakes
object graph snakes

總結:

  • 對象只能保證在單一線程中封裝數據的正確性,在多線程環境下可能會致使狀態混亂,在同一個代碼段,兩個競爭的線程可能致使變量的不一致。

  • 使用鎖看起來能夠在多線程環境下保證封裝數據的正確性,但實際上它在程序真是運行時是低效的而且很容易致使死鎖。

  • 鎖在單機工做可能還不錯,可是在分佈式的環境表現的很不理想,擴展性不好。

共享內存在現代計算機架構上的弊端

在80-90年代的編程模型概念中,寫一個變量至關於直接把它寫入內存,可是在現代的計算機架構中,咱們作了一些改變,寫入相應的緩存中而不是直接寫入內存,大多數緩存都是CPU核心的本地緩存,可是由一個CPU寫入的緩存對其餘CPU是不可見的。爲了讓本地緩存的變化對其餘CPU或者線程可見的話,緩存必須進行交互。

在JVM上,咱們必須使用volatile標識或者Atomic包裝類來保證內存對跨線程的共享,不然,咱們只能用鎖來保證共享內存的正確性。那麼咱們爲何不在全部的變量上都加volatile標識呢?由於在緩存間交互信息是一個代價很是昂貴的操做,並且這個操做會隱式的阻止CPU核心不能去作其餘的工做,而且會致使緩存一致性協議(緩存一致性協議是指CPU用於在主內存和其餘CPU之間傳輸緩存)的瓶頸。

即便開發者認識到這些問題,弄清楚哪些內存位置須要使用volatile標識或者Atomic包裝類,但這並不是是一種很好的解決方案,可能到程序後期,你都不清楚本身作了什麼。

總結:

  • 沒有真正的共享內存了,CPU核心就像網絡上的計算機同樣,將數據塊(高速緩存行)明確地傳遞給彼此。CPU間的通訊和網絡通訊有更多的共同點。 如今經過CPU或網絡計算機傳遞消息是標準的。

  • 使用共享內存標識或者Atomic數據結構來代替隱藏消息傳遞,其實有一種更加規範的方法就是將共享狀態保存在併發實體內,並明確併發實體間經過消息來傳遞事件和數據。

調用堆棧的弊端

今天,咱們還常常調用堆棧來進行任務執行,可是它是在併發並不那麼重要的時代發明的,由於當時多核的CPU系統並不常見。調用堆棧不能跨線程,因此不能進行異步調用。

線程在將任務委託後臺執行會出現一個問題,實際中,是將任務委託給另外一個線程執行,這不是簡單的方法調用,而是有本地的線程直接調用執行,一般來講,一個調用者線程將任務添加到一個內存位置中,具體的工做線程能夠不斷的從中選取任務進行執行,這樣的話,調用者線程沒必要阻塞能夠去作一些其餘的任務了。

可是這裏有幾個問題,第一個就是調用者如何受到任務完成的通知?還有一個更重要的問題是當任務發生異常出現錯誤後,異常會被誰處理?異常將會被具體執行任務的工做線程所處理並不會關心是哪一個調用者調用的任務:

exception pro
exception pro

這是一個很嚴重的問題,具體執行任務的線程是怎麼處理這種情況的?具體執行任務去處理這個問題並非一個好的方案,由於它並不清楚該任務執行的真正目的,並且調用者應該被通知發生了什麼,可是實際上並無這樣的結構去解決這個問題。假如並不能正確的通知,調用者線程將不會的到任何錯誤的信息甚至任務都會丟失。這就比如在網絡上你的請求失敗或者消息丟失卻得不到任何的通知。

在某些狀況,這個問題可能會變得更糟糕,工做線程發生了錯誤可是其自身卻沒法恢復。好比一個由bug引發的內部錯誤致使了線程的關閉,那麼會致使一個問題,到底應該由誰來重啓線程而且保存線程以前的狀態呢?表面上看,這個問題是能夠解決的,但又會有一個新的意外可能發生,當工做線程正在執行任務的時候,它便不能共享任務隊列,而事實上,當一個異常發生後,並逐級上傳,最終可能致使整個任務隊列的狀態所有丟失。因此說即便咱們在本地交互也可能存在消息丟失的狀況。

總結:

  • 實現任何一個高併發且高效性能的系統,線程必須將任務有效率的委託給別的線程執行以致不會阻塞,這種任務委託的併發方式在分佈式的環境也適用,可是須要引入錯誤處理和失敗通知等機制。失敗成爲這種領域模型的一部分。

  • 併發系統適用任務委託機制須要去處理服務故障也就意味須要在發生故障後去恢復服務,但實際狀況下,重啓服務可能會丟失消息,即便沒有發生這種狀況,調用者獲得的迴應也可能由於隊列等待,垃圾回收等影響而延遲,因此,在真正的環境中,咱們須要設置請求回覆的超時時間,就像在網絡系統亦或者分佈式系統。

爲何在高併發,分佈式系統須要Actor模型?

綜上所述,一般的編程模型並不適用現代的高併發分佈式系統,幸運的是,咱們能夠沒必要拋棄咱們瞭解的知識,另外,Actor用很好的方式幫咱們克服了這些問題,它讓咱們以一種更好的模型去實現咱們的系統。

咱們重點需求的是如下幾個方面:

  • 使用封裝,可是不使用鎖。

  • 構建一種實體可以處理消息,更改狀態,發送消息用來推進整個程序運行。

  • 沒必要擔憂程序執行與真實環境的不匹配。

Actor模型能幫咱們實現這些目標,如下是具體描述。

使用消息機制避免使用鎖以防止阻塞

不一樣於方法調用,Actor模型使用消息進行交互。發送消息的方式不會將發送消息方的執行線程轉換爲具體的任務執行線程。Actor能夠不斷的發送和接收消息但不會阻塞。所以它能夠作更多的工做,好比發送消息和接收消息。

在面對對象編程上,直到一個方法返回後,纔會釋放對調用者線程的控制。在這這一方面上,Actor模型跟面對對象模型相似,它對消息作出處理,並在消息處理完成後返回執行。咱們能夠模擬這種執行模式:

actor graph
actor graph

可是這種方式與方法調用方式最大的區別就是沒有返回值。經過發送消息,Actor將任務委託給另外一Actor執行。就想咱們以前說的堆棧調用同樣,加入你須要一個返回值,那麼發送Actor須要阻塞或者與具體執行任務的Actor在同一個線程中。另外,接收Actor以消息的方式返回結果。

第二個關鍵的變化是繼續保持封裝。Actor對消息處理就就跟調用方法同樣,可是不一樣的是,Actor在多線程的狀況下能保證自身內部的狀態和變量不會被破壞,Actor的執行獨立於發送消息的Actor,而且同一個Actor在同一個時刻只處理一個消息。每一個Actor有序的處理接收的消息,因此一個Actor系統中多個Actor能夠併發的處理本身的消息,充分的利用多核CPU。由於一個Actor同一時刻最多處理一個消息,因此它不須要同步機制保障變量的一致性。因此說它並不須要鎖:

serialized timeline invariants
serialized timeline invariants

總而言之,Actor執行的時候會發生如下行爲:

1.Actor將消息加入到消息隊列的尾部。
2.假如一個Actor並未被調度執行,則將其標記爲可執行。
3.一個(對外部不可見)調度器對Actor的執行進行調度。
4.Actor從消息隊列頭部選擇一個消息進行處理。
5.Actor在處理過程當中修改自身的狀態,併發送消息給其餘的Actor。
6.Actor

爲了實現這些行爲,Actor必須有如下特性:

  • 郵箱(做爲一個消息隊列)
  • 行爲(做爲Actor的內部狀態,處理消息邏輯)
  • 消息(請求Actor的數據,可當作方法調用時的參數數據)
  • 執行環境(好比線程池,調度器,消息分發機制等)
  • 位置信息(用於後續可能會發生的行爲)

消息會被添加到Actor的信箱中,Actor的行爲能夠當作Actor是如何對消息作出迴應的(好比發送更多消息或者修改自身狀態)。執行環境提供一組線程池,用於執行Actor的這些行爲操做。

Actor是一個很是簡單的模型並且它能夠解決先前提到的問題:

  • 繼續使用封裝,但經過信號機制保障不需傳遞執行(方法調用須要傳遞執行線程,但發送消息不須要)。

  • 不須要任何的鎖,修改Actor內部的狀態只能經過消息,Actor是串行處理消息,能夠保障內部狀態和變量的正確性。

  • 由於不會再任何地方使用鎖,因此發送者不會被阻塞,成千上萬個Actor能夠被合理的分配在幾十個線程上執行,充分利用了現代CPU的潛力。任務委託這個模式在Actor上很是適用。

  • Actor的狀態是本地的,不可共享的,變化和數據只能經過消息傳遞。

Actor優雅的處理錯誤

Actor再也不使用共享的堆棧調用,因此它要以不一樣的方式去處理錯誤。這裏有兩種錯誤須要考慮:

  • 第一種狀況是當任務委託後再目標Actor上因爲任務自己錯誤而失敗了(典型的如驗證錯誤,好比不存在的用戶ID)。在這個狀況下,Actor服務自己是正確的,只是相應的任務出錯了。服務Actor應該想發送Actor發送消息,已告知錯誤狀況。這裏沒什麼特殊的,錯誤做爲Actor模型的一部分,也能夠當作消息。

  • 第二種狀況是當服務自己遇到內部故障時。Akka強制全部Actor被組織成一個樹狀的層次結構,即建立另外一個Actor的Actor成爲該新Actor的分級。 這與操做系統將流程組合到樹中很是類似。就像進程同樣,當一個Actor失敗時,它的父actor被通知,並對失敗作出反應。此外,若是父actor中止,其全部子Actor也被遞歸中止。這中形式被稱爲監督,它是Akka的核心:

actor tree supervision
actor tree supervision

監管者能夠根據被監管者(子Actor)的失敗的錯誤類型來執行不一樣的策略,好比重啓該Actor或者中止該Actor讓其它Actor代替執行任務。一個Actor不會平白無故的死亡(除非出現死循環之類的狀況),而是失敗,並能夠將失敗傳遞給它的監管者讓其作出相應的故障處理策略,固然也可能會被中止(若被中止,也會接收到相應的消息指令)。一個Actor總有監管者就是它的父級Actor。Actor從新啓動是不可見的,協做Actor能夠幫其代發消息直到目標Actor重啓成功。

相關文章
相關標籤/搜索