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

這段時(shí)間由于忙畢業(yè)前前后后的事情,拖更了很久,表示非常抱歉,回歸后的第一篇文章主要是看到了Akka最新文檔中寫的What problems does the actor model solve?,閱讀完后覺得還是蠻不錯(cuò),能簡潔清晰的闡述目前并發(fā)領(lǐng)域遇到的問題,并為何利用Actor模型可以解決這些問題,本文主要是利用自己的理解將這篇文章進(jìn)行翻譯,有不足之處還請指出。原文鏈接

Actor解決了什么問題?

Akka使用Actor模型來克服傳統(tǒng)面向?qū)ο缶幊棠P偷木窒扌裕?yīng)對高并發(fā)分布式系統(tǒng)所帶來的挑戰(zhàn)。 充分理解Actor模型是必需的,它有助于我們認(rèn)識到傳統(tǒng)的編程方法在并發(fā)和分布式計(jì)算的領(lǐng)域上的不足之處。

封裝的弊端

面向?qū)ο缶幊蹋∣OP)是一種廣泛采用的,熟悉的編程模型,它的一個(gè)核心理念就是封裝,并規(guī)定對象封裝的內(nèi)部數(shù)據(jù)不能從外部直接訪問,只允許相關(guān)的屬性方法進(jìn)行數(shù)據(jù)操作,比如我們熟悉的Javabean中的getX,setX等方法,對象為封裝的內(nèi)部數(shù)據(jù)提供安全的數(shù)據(jù)操作。

舉個(gè)例子,有序二叉樹必須保證樹節(jié)點(diǎn)數(shù)據(jù)的分布規(guī)則,若你想利用有序二叉樹進(jìn)行查詢相關(guān)數(shù)據(jù),就必須要依賴這個(gè)約束。

當(dāng)我們在分析面向?qū)ο缶幊淘谶\(yùn)行時(shí)的行為時(shí),我們可能會繪制一個(gè)消息序列圖,用來顯示方法調(diào)用時(shí)的交互,如下圖所示:

image.png

但上述圖表并不能準(zhǔn)確地表示實(shí)例在執(zhí)行過程中的生命線。實(shí)際上,一個(gè)線程執(zhí)行所有這些調(diào)用,并且變量的操作也在調(diào)用該方法的同一線程上。為剛才的序列圖加上執(zhí)行線程,看起來像這樣:

image.png

但當(dāng)在面對多線程的情況下,會發(fā)現(xiàn)此前的圖越來越混亂和變得不清晰,現(xiàn)在我們模擬多個(gè)線程訪問同一個(gè)示例:

image.png

在上面的這種情況中,兩個(gè)線程調(diào)用同一個(gè)方法,但別調(diào)用的對象并不能保證其封裝的數(shù)據(jù)發(fā)生了什么,兩個(gè)調(diào)用的方法指令可以任意方式的交織,無法保證共享變量的一致性,現(xiàn)在,想象一下在更多線程下這個(gè)問題會更加嚴(yán)重。

解決這個(gè)問題最通常的方法就是在該方法上加鎖。通過加鎖可以保證同一時(shí)刻只有一個(gè)線程能進(jìn)入該方法,但這是一個(gè)代價(jià)非常昂貴的方法:

  • 鎖非常嚴(yán)重的限制并發(fā),它在現(xiàn)在的CPU架構(gòu)上代價(jià)是非常大的,它需要操作系統(tǒng)暫停和重啟線程。

  • 調(diào)用者的線程會被阻塞,以致于它不能去做其他有意義的任務(wù),舉個(gè)例子我們希望桌面程序在后臺運(yùn)行的時(shí)候,操作UI界面也能得到響應(yīng)。在后臺,,線程阻塞完全是浪費(fèi)的,有人可能會說可以通過啟動新線程進(jìn)行補(bǔ)償,但線程也是一種非常昂貴的資源。

  • 使用鎖會導(dǎo)致一個(gè)新的問題:死鎖。

這些現(xiàn)實(shí)存在的問題讓我們只能兩者選一:

  • 不使用鎖,但會導(dǎo)致狀態(tài)混亂。

  • 使用大量的鎖,但是會降低性能并很容易導(dǎo)致死鎖。

另外,鎖只能在本地更好的利用,當(dāng)我們的程序部署在不同的機(jī)器上時(shí),我們只能選擇使用分布式鎖,但不幸的是,分布式鎖的效率可能比本地鎖低好幾個(gè)量級,對后續(xù)的擴(kuò)展也會有很大的限制,分布式鎖的協(xié)議要求多臺機(jī)器在網(wǎng)絡(luò)上進(jìn)行相互通信,因此延遲可能會變得非常高。

在面向?qū)ο笳Z言中,我們很少會去考慮線程或者它的執(zhí)行路徑,我們通常將系統(tǒng)想象成許多實(shí)例對象連接成的網(wǎng)絡(luò),通過方法調(diào)用,修改實(shí)例對象內(nèi)部的狀態(tài),然后通過實(shí)例對象之前的方法調(diào)用驅(qū)動整個(gè)程序進(jìn)行交互:

image.png

然后,在多線程分布式環(huán)境中,實(shí)際上線程是通過方法調(diào)用遍歷這個(gè)對象實(shí)例網(wǎng)絡(luò)。因此,線程是方法調(diào)用驅(qū)動執(zhí)行的:

image.png

總結(jié):

  • 對象只能保證在單一線程中封裝數(shù)據(jù)的正確性,在多線程環(huán)境下可能會導(dǎo)致狀態(tài)混亂,在同一個(gè)代碼段,兩個(gè)競爭的線程可能導(dǎo)致變量的不一致。

  • 使用鎖看起來可以在多線程環(huán)境下保證封裝數(shù)據(jù)的正確性,但實(shí)際上它在程序真是運(yùn)行時(shí)是低效的并且很容易導(dǎo)致死鎖。

  • 鎖在單機(jī)工作可能還不錯(cuò),但是在分布式的環(huán)境表現(xiàn)的很不理想,擴(kuò)展性很差。

共享內(nèi)存在現(xiàn)代計(jì)算機(jī)架構(gòu)上的弊端

在80-90年代的編程模型概念中,寫一個(gè)變量相當(dāng)于直接把它寫入內(nèi)存,但是在現(xiàn)代的計(jì)算機(jī)架構(gòu)中,我們做了一些改變,寫入相應(yīng)的緩存中而不是直接寫入內(nèi)存,大多數(shù)緩存都是CPU核心的本地緩存,但是由一個(gè)CPU寫入的緩存對其他CPU是不可見的。為了讓本地緩存的變化對其他CPU或者線程可見的話,緩存必須進(jìn)行交互。

在JVM上,我們必須使用volatile標(biāo)識或者Atomic包裝類來保證內(nèi)存對跨線程的共享,否則,我們只能用鎖來保證共享內(nèi)存的正確性。那么我們?yōu)槭裁床辉谒械淖兞可隙技觱olatile標(biāo)識呢?因?yàn)樵诰彺骈g交互信息是一個(gè)代價(jià)非常昂貴的操作,而且這個(gè)操作會隱式的阻止CPU核心不能去做其他的工作,并且會導(dǎo)致緩存一致性協(xié)議(緩存一致性協(xié)議是指CPU用于在主內(nèi)存和其他CPU之間傳輸緩存)的瓶頸。

即使開發(fā)者認(rèn)識到這些問題,弄清楚哪些內(nèi)存位置需要使用volatile標(biāo)識或者Atomic包裝類,但這并非是一種很好的解決方案,可能到程序后期,你都不清楚自己做了什么。

總結(jié):

  • 沒有真正的共享內(nèi)存了,CPU核心就像網(wǎng)絡(luò)上的計(jì)算機(jī)一樣,將數(shù)據(jù)塊(高速緩存行)明確地傳遞給彼此。CPU間的通信和網(wǎng)絡(luò)通信有更多的共同點(diǎn)。 現(xiàn)在通過CPU或網(wǎng)絡(luò)計(jì)算機(jī)傳遞消息是標(biāo)準(zhǔn)的。

  • 使用共享內(nèi)存標(biāo)識或者Atomic數(shù)據(jù)結(jié)構(gòu)來代替隱藏消息傳遞,其實(shí)有一種更加規(guī)范的方法就是將共享狀態(tài)保存在并發(fā)實(shí)體內(nèi),并明確并發(fā)實(shí)體間通過消息來傳遞事件和數(shù)據(jù)。

調(diào)用堆棧的弊端

今天,我們還經(jīng)常調(diào)用堆棧來進(jìn)行任務(wù)執(zhí)行,但是它是在并發(fā)并不那么重要的時(shí)代發(fā)明的,因?yàn)楫?dāng)時(shí)多核的CPU系統(tǒng)并不常見。調(diào)用堆棧不能跨線程,所以不能進(jìn)行異步調(diào)用。

線程在將任務(wù)委托后臺執(zhí)行會出現(xiàn)一個(gè)問題,實(shí)際中,是將任務(wù)委托給另一個(gè)線程執(zhí)行,這不是簡單的方法調(diào)用,而是有本地的線程直接調(diào)用執(zhí)行,通常來說,一個(gè)調(diào)用者線程將任務(wù)添加到一個(gè)內(nèi)存位置中,具體的工作線程可以不斷的從中選取任務(wù)進(jìn)行執(zhí)行,這樣的話,調(diào)用者線程不必阻塞可以去做一些其他的任務(wù)了。

但是這里有幾個(gè)問題,第一個(gè)就是調(diào)用者如何受到任務(wù)完成的通知?還有一個(gè)更重要的問題是當(dāng)任務(wù)發(fā)生異常出現(xiàn)錯(cuò)誤后,異常會被誰處理?異常將會被具體執(zhí)行任務(wù)的工作線程所處理并不會關(guān)心是哪個(gè)調(diào)用者調(diào)用的任務(wù):

image.png

這是一個(gè)很嚴(yán)重的問題,具體執(zhí)行任務(wù)的線程是怎么處理這種狀況的?具體執(zhí)行任務(wù)去處理這個(gè)問題并不是一個(gè)好的方案,因?yàn)樗⒉磺宄撊蝿?wù)執(zhí)行的真正目的,而且調(diào)用者應(yīng)該被通知發(fā)生了什么,但是實(shí)際上并沒有這樣的結(jié)構(gòu)去解決這個(gè)問題。假如并不能正確的通知,調(diào)用者線程將不會的到任何錯(cuò)誤的信息甚至任務(wù)都會丟失。這就好比在網(wǎng)絡(luò)上你的請求失敗或者消息丟失卻得不到任何的通知。

在某些情況,這個(gè)問題可能會變得更糟糕,工作線程發(fā)生了錯(cuò)誤但是其自身卻無法恢復(fù)。比如一個(gè)由bug引起的內(nèi)部錯(cuò)誤導(dǎo)致了線程的關(guān)閉,那么會導(dǎo)致一個(gè)問題,到底應(yīng)該由誰來重啟線程并且保存線程之前的狀態(tài)呢?表面上看,這個(gè)問題是可以解決的,但又會有一個(gè)新的意外可能發(fā)生,當(dāng)工作線程正在執(zhí)行任務(wù)的時(shí)候,它便不能共享任務(wù)隊(duì)列,而事實(shí)上,當(dāng)一個(gè)異常發(fā)生后,并逐級上傳,最終可能導(dǎo)致整個(gè)任務(wù)隊(duì)列的狀態(tài)全部丟失。所以說即使我們在本地交互也可能存在消息丟失的情況。

總結(jié):

  • 實(shí)現(xiàn)任何一個(gè)高并發(fā)且高效性能的系統(tǒng),線程必須將任務(wù)有效率的委托給別的線程執(zhí)行以至不會阻塞,這種任務(wù)委托的并發(fā)方式在分布式的環(huán)境也適用,但是需要引入錯(cuò)誤處理和失敗通知等機(jī)制。失敗成為這種領(lǐng)域模型的一部分。

  • 并發(fā)系統(tǒng)適用任務(wù)委托機(jī)制需要去處理服務(wù)故障也就意味需要在發(fā)生故障后去恢復(fù)服務(wù),但實(shí)際情況下,重啟服務(wù)可能會丟失消息,即使沒有發(fā)生這種情況,調(diào)用者得到的回應(yīng)也可能因?yàn)殛?duì)列等待,垃圾回收等影響而延遲,所以,在真正的環(huán)境中,我們需要設(shè)置請求回復(fù)的超時(shí)時(shí)間,就像在網(wǎng)絡(luò)系統(tǒng)亦或者分布式系統(tǒng)。

為什么在高并發(fā),分布式系統(tǒng)需要Actor模型?

綜上所述,通常的編程模型并不適用現(xiàn)代的高并發(fā)分布式系統(tǒng),幸運(yùn)的是,我們可以不必拋棄我們了解的知識,另外,Actor用很好的方式幫我們克服了這些問題,它讓我們以一種更好的模型去實(shí)現(xiàn)我們的系統(tǒng)。

我們重點(diǎn)需求的是以下幾個(gè)方面:

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

  • 構(gòu)建一種實(shí)體能夠處理消息,更改狀態(tài),發(fā)送消息用來推動整個(gè)程序運(yùn)行。

  • 不必?fù)?dān)心程序執(zhí)行與真實(shí)環(huán)境的不匹配。

Actor模型能幫我們實(shí)現(xiàn)這些目標(biāo),以下是具體描述。

使用消息機(jī)制避免使用鎖以防止阻塞

不同于方法調(diào)用,Actor模型使用消息進(jìn)行交互。發(fā)送消息的方式不會將發(fā)送消息方的執(zhí)行線程轉(zhuǎn)換為具體的任務(wù)執(zhí)行線程。Actor可以不斷的發(fā)送和接收消息但不會阻塞。因此它可以做更多的工作,比如發(fā)送消息和接收消息。

在面對對象編程上,直到一個(gè)方法返回后,才會釋放對調(diào)用者線程的控制。在這這一方面上,Actor模型跟面對對象模型類似,它對消息做出處理,并在消息處理完成后返回執(zhí)行。我們可以模擬這種執(zhí)行模式:

image.png

但是這種方式與方法調(diào)用方式最大的區(qū)別就是沒有返回值。通過發(fā)送消息,Actor將任務(wù)委托給另一Actor執(zhí)行。就想我們之前說的堆棧調(diào)用一樣,加入你需要一個(gè)返回值,那么發(fā)送Actor需要阻塞或者與具體執(zhí)行任務(wù)的Actor在同一個(gè)線程中。另外,接收Actor以消息的方式返回結(jié)果。

第二個(gè)關(guān)鍵的變化是繼續(xù)保持封裝。Actor對消息處理就就跟調(diào)用方法一樣,但是不同的是,Actor在多線程的情況下能保證自身內(nèi)部的狀態(tài)和變量不會被破壞,Actor的執(zhí)行獨(dú)立于發(fā)送消息的Actor,并且同一個(gè)Actor在同一個(gè)時(shí)刻只處理一個(gè)消息。每個(gè)Actor有序的處理接收的消息,所以一個(gè)Actor系統(tǒng)中多個(gè)Actor可以并發(fā)的處理自己的消息,充分的利用多核CPU。因?yàn)橐粋€(gè)Actor同一時(shí)刻最多處理一個(gè)消息,所以它不需要同步機(jī)制保障變量的一致性。所以說它并不需要鎖:

image.png

總而言之,Actor執(zhí)行的時(shí)候會發(fā)生以下行為:

1.Actor將消息加入到消息隊(duì)列的尾部。
2.假如一個(gè)Actor并未被調(diào)度執(zhí)行,則將其標(biāo)記為可執(zhí)行。
3.一個(gè)(對外部不可見)調(diào)度器對Actor的執(zhí)行進(jìn)行調(diào)度。
4.Actor從消息隊(duì)列頭部選擇一個(gè)消息進(jìn)行處理。
5.Actor在處理過程中修改自身的狀態(tài),并發(fā)送消息給其他的Actor。
6.Actor

為了實(shí)現(xiàn)這些行為,Actor必須有以下特性:

  • 郵箱(作為一個(gè)消息隊(duì)列)
  • 行為(作為Actor的內(nèi)部狀態(tài),處理消息邏輯)
  • 消息(請求Actor的數(shù)據(jù),可看成方法調(diào)用時(shí)的參數(shù)數(shù)據(jù))
  • 執(zhí)行環(huán)境(比如線程池,調(diào)度器,消息分發(fā)機(jī)制等)
  • 位置信息(用于后續(xù)可能會發(fā)生的行為)

消息會被添加到Actor的信箱中,Actor的行為可以看成Actor是如何對消息做出回應(yīng)的(比如發(fā)送更多消息或者修改自身狀態(tài))。執(zhí)行環(huán)境提供一組線程池,用于執(zhí)行Actor的這些行為操作。

Actor是一個(gè)非常簡單的模型而且它可以解決先前提到的問題:

  • 繼續(xù)使用封裝,但通過信號機(jī)制保障不需傳遞執(zhí)行(方法調(diào)用需要傳遞執(zhí)行線程,但發(fā)送消息不需要)。

  • 不需要任何的鎖,修改Actor內(nèi)部的狀態(tài)只能通過消息,Actor是串行處理消息,可以保障內(nèi)部狀態(tài)和變量的正確性。

  • 因?yàn)椴粫偃魏蔚胤绞褂面i,所以發(fā)送者不會被阻塞,成千上萬個(gè)Actor可以被合理的分配在幾十個(gè)線程上執(zhí)行,充分利用了現(xiàn)代CPU的潛力。任務(wù)委托這個(gè)模式在Actor上非常適用。

  • Actor的狀態(tài)是本地的,不可共享的,變化和數(shù)據(jù)只能通過消息傳遞。

Actor優(yōu)雅的處理錯(cuò)誤

Actor不再使用共享的堆棧調(diào)用,所以它要以不同的方式去處理錯(cuò)誤。這里有兩種錯(cuò)誤需要考慮:

  • 第一種情況是當(dāng)任務(wù)委托后再目標(biāo)Actor上由于任務(wù)本身錯(cuò)誤而失敗了(典型的如驗(yàn)證錯(cuò)誤,比如不存在的用戶ID)。在這個(gè)情況下,Actor服務(wù)本身是正確的,只是相應(yīng)的任務(wù)出錯(cuò)了。服務(wù)Actor應(yīng)該想發(fā)送Actor發(fā)送消息,已告知錯(cuò)誤情況。這里沒什么特殊的,錯(cuò)誤作為Actor模型的一部分,也可以當(dāng)做消息。

  • 第二種情況是當(dāng)服務(wù)本身遇到內(nèi)部故障時(shí)。Akka強(qiáng)制所有Actor被組織成一個(gè)樹狀的層次結(jié)構(gòu),即創(chuàng)建另一個(gè)Actor的Actor成為該新Actor的分級。 這與操作系統(tǒng)將流程組合到樹中非常相似。就像進(jìn)程一樣,當(dāng)一個(gè)Actor失敗時(shí),它的父actor被通知,并對失敗做出反應(yīng)。此外,如果父actor停止,其所有子Actor也被遞歸停止。這中形式被稱為監(jiān)督,它是Akka的核心:

image.png

監(jiān)管者可以根據(jù)被監(jiān)管者(子Actor)的失敗的錯(cuò)誤類型來執(zhí)行不同的策略,比如重啟該Actor或者停止該Actor讓其它Actor代替執(zhí)行任務(wù)。一個(gè)Actor不會無緣無故的死亡(除非出現(xiàn)死循環(huán)之類的情況),而是失敗,并可以將失敗傳遞給它的監(jiān)管者讓其做出相應(yīng)的故障處理策略,當(dāng)然也可能會被停止(若被停止,也會接收到相應(yīng)的消息指令)。一個(gè)Actor總有監(jiān)管者就是它的父級Actor。Actor被重新啟動是不可見的,協(xié)作Actor可以幫其代發(fā)消息直到目標(biāo)Actor重啟成功。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容