摘要:現在高要求的分佈式系統的建造者遇到了不能徹底由傳統的面向對象編程(OOP)模型解決的挑戰,但這能夠從Actor模型中獲益。
Actor模型做爲一種高性能網絡中的並行處理方式由Carl Hewitt幾十年前提出-高性能網絡環境在當時還不可用。現在,硬件和基礎設施的能力已經遇上並超越了Hewitt的願景。所以,高要求的分佈式系統的建造者遇到了不能徹底由傳統的面向對象編程(OOP)模型解決的挑戰,但這能夠從Actor模型中獲益。
今天,Actor模型不只被認爲是高效的解決方案——這已經被世界上要求最高的應用所檢驗。爲了突出Actor模型解決的問題,這個主題討論如下傳統編程的假設與現代多線程、多CPU體系架構之間的不匹配:html
OOP的一個核心支柱是封裝。封裝代表一個對象的內部狀態不能直接從外部訪問;它只能夠經過調用一組輔助的方法修改。對象負責暴露保護它所封裝數據的不變性的安全操做。例如,在一個有序二叉樹上的操做不容許違反樹的有序性。調用者但願保持有序性,當查詢樹上一條特定的數據時,它們須要可以依賴這個約束。
當分析OOP運行時的行爲時,咱們有時候畫出一個消息序列圖展現方法調用的交互過程。例如:編程
不幸的是,上面的圖表沒能精確表示執行過程當中對象的生命線。實際上,一個線程執行全部的調用,全部對象的不變體約束出如今同一個方法被調用的線程中。更新線程執行圖,它看起來是這樣:緩存
當試圖對多線程行爲建模時,上面闡述的重要性變得明顯了。忽然,咱們畫出的簡潔的圖表變得不夠充分了。咱們能夠嘗試解釋多線程訪問同一對象:安全
有一個執行部分,兩個線程調用同一個方法。不幸的是,對象的封裝模型不能保證執行這部分時會發生什麼。兩個線程之間沒有某種協調的話,兩個調用指令將以不能保證不變體性質的任意方式相互交錯。如今,想象一下這個由多個線程存在而變得複雜的問題。網絡
解決這個問題的常見方法是給這些方法加一個鎖。儘管這保證了在給定的時間內最多一個線程將執行該方法,可是這是一個代價高昂的策略: 數據結構
這些事實致使一個沒法取勝的局面:多線程
另外,鎖只有在本地有用。當涉及跨機器協調時,惟一可選的是分佈式鎖。不幸的是,分佈式鎖比本地鎖低效幾個數量級,而且限制了伸縮性。分佈式鎖協議須要在網絡中跨機器的多輪通訊,所以延遲飛漲。架構
在面嚮對象語言中,咱們一般不多考慮線路或線性執行路徑。咱們常常把系統想象成一個對象實例的網絡,這些實例對象響應方法調用、修改自身內部狀態、而後經過方法調用相互通訊以驅動整個應用狀態向前:併發
然而,在一個多線程的分佈式環境中,實際發生的是線程沿着方法調用貫穿這個對象實例網絡。所以,線程是真正的運行驅動者:異步
【總結】
80-90年代的編程模型定義:寫入一個變量意味着直接寫到內存位置 (這在必定程上混淆了局部變量可能僅存在於寄存器)。在現代體系架構中,若是咱們簡化一下,CPUs會寫到cache行而不是直接寫入內存。大多數caches是CPU局部私有的,也就是,一個核寫入變量不會被其餘核看到。爲了使局部改變對其餘核可見,所以對於另外一個線程,cache行須要被傳送到其餘核的cache。
在JVM中,咱們必須經過使用volatile或Atomic顯式地指示線程間共享的內存位置。不然,咱們只能在鎖定的部分訪問這些內存。爲何咱們不將全部變量標記爲volatile?由於跨核傳送cache行是一個代價很是高昂的操做!這樣作會隱式地中止涉及作額外工做的核,並致使緩存一致性協議的瓶頸。(CPUs用於主存和其餘CPUs之間傳輸cache行的協議)。結果即是下降數量級的運行速度。
即便對於瞭解這個狀況的開發者,搞清楚哪一個內存位置應該被標記爲volatile或者使用哪種原子結構是一門黑暗的藝術。
【總結】
今天,咱們經常將調用棧視爲理所固然。可是,調用棧是在一個併發程序不那麼重要的時代發明的,由於多CPU系統那時不常見。調用棧沒有跨越線程,於是沒有對異步調用鏈建模。
當一個線程意圖委派一個任務給後臺的時候會出現問題。實際上,這意味着委託給另外一個線程。這不是一個簡單的方法、函數調用,由於調用嚴格上屬於線程內部。一般,調用者(caller)線程將一個對象放入與一個工做線程(callee)共享的內存位置,反過來,這個工做線程(callee)在某個循環事件中獲取這個對象。這使得調用者(caller)線程能夠向前運行和執行其餘任務。
第一個問題是:調用者(caller)線程如何被通知任務完成了?可是當一個任務失敗且帶有異常的時候一個更嚴重問題出現了。異常應該傳播到哪裏?異常將被傳播到工做者(worker)線程的異常處理器而徹底忽略誰是真正的調用者(caller):
這是一個嚴重的問題。工做者(worker)線程如何處理這種狀況?它可能沒法解決這個問題,由於它一般不知道失敗任務的目的。調用者(caller)線程須要以某種方式被通知,可是沒有調用棧去返回一個異常。失敗通知只能經過邊信道完成,例如,將一個錯誤代碼放在調用者(caller)線程本來期待結果準備好的地方。若是這個通知不到位,調用者(caller)線程不會被通知任務失敗和丟失!這和網絡系統的工做方式驚人地類似-網絡系統中的消息和請求能夠丟失或失敗而沒有任何通知。
在任務出錯和一個工做者(worker)線程遇到一個bug並不可恢復的時候,這個糟糕的狀況會變得更糟。例如,一個由bug引發的內部異常向上傳遞到工做者(worker)線程的根部並使該線程關閉。這當即產生一個疑問,誰應該重啓由該線程持有的這一服務的正常操做,以及怎樣將它恢復到一個已知的良好狀態?乍一看,這彷佛很容易,可是咱們忽然遇到一個新的、意外的現象:線程正在執行的實際任務已經不在任務被取走得共享內存位置了 (一般是一個隊列)。事實上,因爲異常到達頂部,展開全部的調用棧,任務狀態徹底丟失了!咱們已經丟失了一條消息,儘管這是本地的通訊,沒有涉及到網絡 (消息丟失是可指望的)。
【總結】
本文翻譯自https://doc.akka.io/docs/akka/current/guide/actors-motivation.html
本文分享自華爲雲社區《【Akka系列】之 爲何現代系統須要一個新的編程模型? 》,原文做者:荔子 。