摘要:原文鏈接解決了什么問題使用模型來克服傳統面向對象編程模型的局限性,并應對高并發分布式系統所帶來的挑戰。在某些情況,這個問題可能會變得更糟糕,工作線程發生了錯誤但是其自身卻無法恢復。
這段時間由于忙畢業前前后后的事情,拖更了很久,表示非常抱歉,回歸后的第一篇文章主要是看到了Akka最新文檔中寫的What problems does the actor model solve?,閱讀完后覺得還是蠻不錯,能簡潔清晰的闡述目前并發領域遇到的問題,并為何利用Actor模型可以解決這些問題,本文主要是利用自己的理解將這篇文章進行翻譯,有不足之處還請指出。原文鏈接
Actor解決了什么問題?Akka使用Actor模型來克服傳統面向對象編程模型的局限性,并應對高并發分布式系統所帶來的挑戰。 充分理解Actor模型是必需的,它有助于我們認識到傳統的編程方法在并發和分布式計算的領域上的不足之處。
封裝的弊端面向對象編程(OOP)是一種廣泛采用的,熟悉的編程模型,它的一個核心理念就是封裝,并規定對象封裝的內部數據不能從外部直接訪問,只允許相關的屬性方法進行數據操作,比如我們熟悉的Javabean中的getX,setX等方法,對象為封裝的內部數據提供安全的數據操作。
舉個例子,有序二叉樹必須保證樹節點數據的分布規則,若你想利用有序二叉樹進行查詢相關數據,就必須要依賴這個約束。
當我們在分析面向對象編程在運行時的行為時,我們可能會繪制一個消息序列圖,用來顯示方法調用時的交互,如下圖所示:
但上述圖表并不能準確地表示實例在執行過程中的生命線。實際上,一個線程執行所有這些調用,并且變量的操作也在調用該方法的同一線程上。為剛才的序列圖加上執行線程,看起來像這樣:
但當在面對多線程的情況下,會發現此前的圖越來越混亂和變得不清晰,現在我們模擬多個線程訪問同一個示例:
在上面的這種情況中,兩個線程調用同一個方法,但別調用的對象并不能保證其封裝的數據發生了什么,兩個調用的方法指令可以任意方式的交織,無法保證共享變量的一致性,現在,想象一下在更多線程下這個問題會更加嚴重。
解決這個問題最通常的方法就是在該方法上加鎖。通過加鎖可以保證同一時刻只有一個線程能進入該方法,但這是一個代價非常昂貴的方法:
鎖非常嚴重的限制并發,它在現在的CPU架構上代價是非常大的,它需要操作系統暫停和重啟線程。
調用者的線程會被阻塞,以致于它不能去做其他有意義的任務,舉個例子我們希望桌面程序在后臺運行的時候,操作UI界面也能得到響應。在后臺,,線程阻塞完全是浪費的,有人可能會說可以通過啟動新線程進行補償,但線程也是一種非常昂貴的資源。
使用鎖會導致一個新的問題:死鎖。
這些現實存在的問題讓我們只能兩者選一:
不使用鎖,但會導致狀態混亂。
使用大量的鎖,但是會降低性能并很容易導致死鎖。
另外,鎖只能在本地更好的利用,當我們的程序部署在不同的機器上時,我們只能選擇使用分布式鎖,但不幸的是,分布式鎖的效率可能比本地鎖低好幾個量級,對后續的擴展也會有很大的限制,分布式鎖的協議要求多臺機器在網絡上進行相互通信,因此延遲可能會變得非常高。
在面向對象語言中,我們很少會去考慮線程或者它的執行路徑,我們通常將系統想象成許多實例對象連接成的網絡,通過方法調用,修改實例對象內部的狀態,然后通過實例對象之前的方法調用驅動整個程序進行交互:
然后,在多線程分布式環境中,實際上線程是通過方法調用遍歷這個對象實例網絡。因此,線程是方法調用驅動執行的:
總結:
對象只能保證在單一線程中封裝數據的正確性,在多線程環境下可能會導致狀態混亂,在同一個代碼段,兩個競爭的線程可能導致變量的不一致。
使用鎖看起來可以在多線程環境下保證封裝數據的正確性,但實際上它在程序真是運行時是低效的并且很容易導致死鎖。
鎖在單機工作可能還不錯,但是在分布式的環境表現的很不理想,擴展性很差。
共享內存在現代計算機架構上的弊端在80-90年代的編程模型概念中,寫一個變量相當于直接把它寫入內存,但是在現代的計算機架構中,我們做了一些改變,寫入相應的緩存中而不是直接寫入內存,大多數緩存都是CPU核心的本地緩存,但是由一個CPU寫入的緩存對其他CPU是不可見的。為了讓本地緩存的變化對其他CPU或者線程可見的話,緩存必須進行交互。
在JVM上,我們必須使用volatile標識或者Atomic包裝類來保證內存對跨線程的共享,否則,我們只能用鎖來保證共享內存的正確性。那么我們為什么不在所有的變量上都加volatile標識呢?因為在緩存間交互信息是一個代價非常昂貴的操作,而且這個操作會隱式的阻止CPU核心不能去做其他的工作,并且會導致緩存一致性協議(緩存一致性協議是指CPU用于在主內存和其他CPU之間傳輸緩存)的瓶頸。
即使開發者認識到這些問題,弄清楚哪些內存位置需要使用volatile標識或者Atomic包裝類,但這并非是一種很好的解決方案,可能到程序后期,你都不清楚自己做了什么。
總結:
沒有真正的共享內存了,CPU核心就像網絡上的計算機一樣,將數據塊(高速緩存行)明確地傳遞給彼此。CPU間的通信和網絡通信有更多的共同點。 現在通過CPU或網絡計算機傳遞消息是標準的。
使用共享內存標識或者Atomic數據結構來代替隱藏消息傳遞,其實有一種更加規范的方法就是將共享狀態保存在并發實體內,并明確并發實體間通過消息來傳遞事件和數據。
調用堆棧的弊端今天,我們還經常調用堆棧來進行任務執行,但是它是在并發并不那么重要的時代發明的,因為當時多核的CPU系統并不常見。調用堆棧不能跨線程,所以不能進行異步調用。
線程在將任務委托后臺執行會出現一個問題,實際中,是將任務委托給另一個線程執行,這不是簡單的方法調用,而是有本地的線程直接調用執行,通常來說,一個調用者線程將任務添加到一個內存位置中,具體的工作線程可以不斷的從中選取任務進行執行,這樣的話,調用者線程不必阻塞可以去做一些其他的任務了。
但是這里有幾個問題,第一個就是調用者如何受到任務完成的通知?還有一個更重要的問題是當任務發生異常出現錯誤后,異常會被誰處理?異常將會被具體執行任務的工作線程所處理并不會關心是哪個調用者調用的任務:
這是一個很嚴重的問題,具體執行任務的線程是怎么處理這種狀況的?具體執行任務去處理這個問題并不是一個好的方案,因為它并不清楚該任務執行的真正目的,而且調用者應該被通知發生了什么,但是實際上并沒有這樣的結構去解決這個問題。假如并不能正確的通知,調用者線程將不會的到任何錯誤的信息甚至任務都會丟失。這就好比在網絡上你的請求失敗或者消息丟失卻得不到任何的通知。
在某些情況,這個問題可能會變得更糟糕,工作線程發生了錯誤但是其自身卻無法恢復。比如一個由bug引起的內部錯誤導致了線程的關閉,那么會導致一個問題,到底應該由誰來重啟線程并且保存線程之前的狀態呢?表面上看,這個問題是可以解決的,但又會有一個新的意外可能發生,當工作線程正在執行任務的時候,它便不能共享任務隊列,而事實上,當一個異常發生后,并逐級上傳,最終可能導致整個任務隊列的狀態全部丟失。所以說即使我們在本地交互也可能存在消息丟失的情況。
總結:
實現任何一個高并發且高效性能的系統,線程必須將任務有效率的委托給別的線程執行以至不會阻塞,這種任務委托的并發方式在分布式的環境也適用,但是需要引入錯誤處理和失敗通知等機制。失敗成為這種領域模型的一部分。
并發系統適用任務委托機制需要去處理服務故障也就意味需要在發生故障后去恢復服務,但實際情況下,重啟服務可能會丟失消息,即使沒有發生這種情況,調用者得到的回應也可能因為隊列等待,垃圾回收等影響而延遲,所以,在真正的環境中,我們需要設置請求回復的超時時間,就像在網絡系統亦或者分布式系統。
為什么在高并發,分布式系統需要Actor模型?綜上所述,通常的編程模型并不適用現代的高并發分布式系統,幸運的是,我們可以不必拋棄我們了解的知識,另外,Actor用很好的方式幫我們克服了這些問題,它讓我們以一種更好的模型去實現我們的系統。
我們重點需求的是以下幾個方面:
使用封裝,但是不使用鎖。
構建一種實體能夠處理消息,更改狀態,發送消息用來推動整個程序運行。
不必擔心程序執行與真實環境的不匹配。
Actor模型能幫我們實現這些目標,以下是具體描述。
使用消息機制避免使用鎖以防止阻塞不同于方法調用,Actor模型使用消息進行交互。發送消息的方式不會將發送消息方的執行線程轉換為具體的任務執行線程。Actor可以不斷的發送和接收消息但不會阻塞。因此它可以做更多的工作,比如發送消息和接收消息。
在面對對象編程上,直到一個方法返回后,才會釋放對調用者線程的控制。在這這一方面上,Actor模型跟面對對象模型類似,它對消息做出處理,并在消息處理完成后返回執行。我們可以模擬這種執行模式:
但是這種方式與方法調用方式最大的區別就是沒有返回值。通過發送消息,Actor將任務委托給另一Actor執行。就想我們之前說的堆棧調用一樣,加入你需要一個返回值,那么發送Actor需要阻塞或者與具體執行任務的Actor在同一個線程中。另外,接收Actor以消息的方式返回結果。
第二個關鍵的變化是繼續保持封裝。Actor對消息處理就就跟調用方法一樣,但是不同的是,Actor在多線程的情況下能保證自身內部的狀態和變量不會被破壞,Actor的執行獨立于發送消息的Actor,并且同一個Actor在同一個時刻只處理一個消息。每個Actor有序的處理接收的消息,所以一個Actor系統中多個Actor可以并發的處理自己的消息,充分的利用多核CPU。因為一個Actor同一時刻最多處理一個消息,所以它不需要同步機制保障變量的一致性。所以說它并不需要鎖:
總而言之,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)的失敗的錯誤類型來執行不同的策略,比如重啟該Actor或者停止該Actor讓其它Actor代替執行任務。一個Actor不會無緣無故的死亡(除非出現死循環之類的情況),而是失敗,并可以將失敗傳遞給它的監管者讓其做出相應的故障處理策略,當然也可能會被停止(若被停止,也會接收到相應的消息指令)。一個Actor總有監管者就是它的父級Actor。Actor被重新啟動是不可見的,協作Actor可以幫其代發消息直到目標Actor重啟成功。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/67327.html
摘要:是一個構建在上,基于模型的的并發框架,為構建伸縮性強,有彈性的響應式并發應用提高更好的平臺。上述例子中的信件就相當于中的消息,與之間只能通過消息通信。當然模型比這要復雜的多,這里主要是簡潔的闡述一下模型的概念。模型的出現解決了這個問題。 Akka是一個構建在JVM上,基于Actor模型的的并發框架,為構建伸縮性強,有彈性的響應式并發應用提高更好的平臺。本文主要是個人對Akka的學習和應...
摘要:共享內存相信對并發有所了解的同學都應該知道在推出后,對內存管理有了更高標準的規范了,這使我們開發并發程序也有更好的標準了,不會有一些模糊的定義導致的無法確定的錯誤。 通過前幾篇的學習,相信大家對Akka應該有所了解了,都說解決并發哪家強,JVM上面找Akka,那么Akka到底在解決并發問題上幫我們做了什么呢? 共享內存 眾所周知,在處理并發問題上面,最核心的一部分就是如何處理共享內存,...
摘要:源碼鏈接進階持久化插件有同學可能會問,我對不是很熟悉亦或者覺得單機存儲并不是安全,有沒有支持分布式數據存儲的插件呢,比如某爸的云數據庫答案當然是有咯,良心的我當然是幫你們都找好咯。 這次把這部分內容提到現在寫,是因為這段時間開發的項目剛好在這一塊遇到了一些難點,所以準備把經驗分享給大家,我們在使用Akka時,會經常遇到一些存儲Actor內部狀態的場景,在系統正常運行的情況下,我們不需要...
摘要:模型作為中最核心的概念,所以在中的組織結構也至關重要,本文主要介紹中系統。這里主要是演示可以根據配置文件的內容去加載相應的環境,并應用到整個中,這對于我們配置環境來說是非常方便的。路徑與地址熟悉類系統的同學應該對路徑這個概念很熟悉了。 Actor模型作為Akka中最核心的概念,所以Actor在Akka中的組織結構也至關重要,本文主要介紹Akka中Actor系統。 Actor系統 Act...
摘要:是所有由系統創建的頂級的監管者,如日志監聽器,或由配置指定在系統啟動時自動部署的。所有其他被上升到根監管者,然后整個系統將會關閉。監管容錯示例本示例主要演示在發生錯誤時,它的監管者會根據相應的監管策略進行不同的處理。 Akka作為一種成熟的生產環境并發解決方案,必須擁有一套完善的錯誤異常處理機制,本文主要講講Akka中的監管和容錯。 監管 看過我上篇文章的同學應該對Actor系統的工作...
閱讀 2848·2021-11-15 11:39
閱讀 1823·2021-09-24 09:48
閱讀 1065·2021-09-22 15:36
閱讀 3590·2021-09-10 11:22
閱讀 3006·2021-09-07 09:59
閱讀 958·2021-09-03 10:28
閱讀 673·2021-09-02 15:15
閱讀 2744·2021-08-27 16:24