天天看點

Actor模型學習

最近看到了一篇寫的賊好的blog,講的完全詳細,看得出來筆者的功力,是以趕緊轉載過來,最下面有原文的位址。

大家一起共勉!

傳統的遊戲伺服器要麼是單線程要麼是多線程,過去幾十年裡CPU一直遵循摩爾定律發展,帶來的結果是單核頻率越來越高。而近幾年摩爾定義在CPU上已然失效,為什麼呢?

大于在2003年左右,計算機的核心特性經曆了一個重要的變化,處理器的速度達到了一個頂點。在接下來近15年裡,時脈速度是呈線性增長的,而不會像以前那樣以指數級的速度增長。

由于CPU的工藝制程和發熱穩定性之間難以取舍,取而代之的政策是增加CPU核心的數量。多核處理器應運而生,計算處理變成了團隊協作,效率的提升通過多個核心的通信來實作,而不是傳統的時脈速度的提升。這也是線程發揮作用的地方。

目前家用PC四核已經非常常見,伺服器更是達到32核64線程。為了高效的利用多核CPU,應該在代碼層面就考慮并發性。經過十幾年痛苦的開發經曆,事實告訴我們線程并不是擷取并發性的好方法,而往往會帶來難以查找的問題。

例如:以稀缺資源的計數為例,如商品的庫存數量或活動的可售門票,可能存在多個請求同時擷取一個或多個商品或門票。考慮常用實作方式,每個請求對應一個線程,很可能會有多個并發運作的線程都去調整計數器。模型必須確定在同一時間隻能有一個線程去遞減計數器的值。這樣做的原因是因為遞減操作存在兩個步驟:首先檢查目前計數器,確定計數器的值大于或等于要減少的值。其次遞減計數器。

為什麼要将兩步操作作為一個整體操作來完成呢?

因為每個請求代表購買一個或多個,假設有兩個線程并發地調整計數器,若計數器目前為10, 線程1要想計數器遞減2,線程2想要計數器遞減9,線程1和線程2都會檢查目前計數器的值,而計數器的值均大于要遞減的數量。是以線程1和線程2都會繼續運作并遞減計數器的值,最後的結果是多少呢?10-2-9=-1,問題來了。這樣的結果直接操作庫存被過度配置設定,違反了業務規則。

為了防止過度配置設定,原生的方式是将檢查和遞減兩步操作放到一個原子操作中,将兩步操作鎖定到一個操作中,就能夠消除過度配置設定的可能性。

例如,兩個線程同時嘗試購買最後一件商品時,如果沒有鎖就可能出現多個線程同時斷定計數器的值大于或等于購買數量,然後錯誤地遞減計數器,進而導緻出現負數。

然而,問題的根源在于一個請求對應一個線程。

另外,在高度競争的階段,很有可能出現很長的線程隊列,他們都在等待遞減計數器。但使用隊列的方式的問題在于可能造成衆多阻塞線程,也就是每個線程都在等待輪到它們去執行一個序列化的操作。

是以,應用設計者一不小心,内在的複雜性就有可能将多核多線程的應用變成單線程的應用,或者導緻工作線程之間存在高度競争。

Actor模型優雅的解決了這個難題,為真正多線程的應用提供了一個基礎支援。

為什麼會出現Actor這種并發程式設計的模型呢?

關于這一點需要先說說并發性中的一緻性和隔離性,一緻性是讓資料保持一緻,例如銀行轉賬的場景中,轉賬完成時雙方賬戶必須是一方減少一方增加。而隔離性而可以了解為犧牲一部分一緻性需求,進而獲得性能的提升。例如,在完全一緻性的情況下,任務是串行的,此時也就不存在隔離性了。

那為什麼會有Actor模型呢?

因為傳統并發模式中,共享記憶體是傾向于強一緻性弱隔離性的,例如悲觀鎖同步的方式就是使用強一緻性的方式控制并發,而Actor模型天然是強隔離性且弱一緻性的,是以Actor模型在并發中有良好的性能,而且易于控制和管理。

Actor模型的設計是消息驅動和非阻塞的,吞吐量自然也被考慮在内。

Actor模型适用于對一緻性需求不是很高且對性能需求較高的場景

綜上所述,計算機CPU的計算速度(頻率)的提高是有限的,剩下能做的是放入多個計算核心以提升性能。為了利用多核心的性能,需要并發執行。但多線程的方式往往會引入很多問題,同時直接增加了調試難度。

為什麼Actor模型是一種處理并發問題的解決方案呢?

處理并發問題一貫的思路是如何保證共享資料的一緻性和正确性。

一般而言,有兩種政策用來在并發線程中進行通信:共享資料、消息傳遞

使用共享資料的并發程式設計面臨的最大問題是資料條件競争data race,處理各種鎖的問題是讓人十分頭疼的。和共享資料方式相比,消息傳遞機制最大的優勢在于不會産生資料競争狀态。而實作消息傳遞有兩種常見類型:基于channel的消息傳遞、基于Actor的消息傳遞。

為什麼要保持共享資料的正确性呢?

無非是因為程式是多線程的,多個線程對同一個資料操作時若不加入同步條件,勢必造成資料污染。

那麼為什麼不能使用單線程去處理請求呢?

大部分人認為單線程處理相比多線程而言,系統的性能将大打折扣。Actor模型的出現解決了這些問題。

程序間通信

把通信的線程可以想象成兩個無法直接說話而必須通過郵件交流的人,雙方要交流就要發送郵件。發送方郵件一旦發出就不能修改任何内容,而且是沒有辦法收回修改後再發的,這也就是消息一旦發出就不可改變。對于接收方而言,想什麼時候看郵件就什麼時候看,而且不需要監聽,這就叫異步。接收方看了發送方的郵件可以回複也可以撒都不做。隻是回複郵件一旦發出也同樣是不能收回修改的,也就是不可變性兩端都是一樣的。同樣,發送方針對回複郵件,也是想什麼時候看就什麼時候看。兩端同樣都是異步的。這種通信模型就是Actor想要的模型,可以發現這種通信方式其實依賴一套郵件系統或叫做消息管理系統。程序内部要有一套這樣的系統,給每個線程一個獨立的收發消息的管道,并且都是異步的。

并發性

并發導緻最大的問題是對共享資料的操作,面對并發問題時多采用鎖去保證共享資料的一緻性,但同樣也會帶來一系列的副作用,比如要去考慮鎖的粒度(對方法、程式塊等)、鎖的形式(讀鎖、寫鎖等)等問題。

傳統的并發程式設計的方式大多使用鎖機制,相信大多數都是悲觀鎖,這幾乎可以斷定會出現兩個非常明顯的問題:随着項目體量增大,業務愈加複雜,不可避免地會大量的使用鎖,然而鎖的機制其實是很低效的。即使大量依賴鎖解決了項目中資源競争的情況,但由于沒有一個規範的程式設計模式,最後系統的穩定性肯定會出問題,最根本的原因是沒有把系統的任務排程抽象出來,由于任務排程和業務邏輯耦合在一起,很難做一個很高層的抽象以保證任務排程有序性。

Actor模型為并發而生,是為解決高并發的一種程式設計思路。使用并發程式設計時需要特别關注鎖與記憶體原子性等一系列的線程問題,Actor模型内部的狀态由自身維護,也就是說Actor内部資料隻能由它自己通過消息傳遞來進行狀态修改,是以使用Actor模型可以很好地避免這些問題。

Actor為什麼一定程度上可以解決這些問題呢?

因為Actor模型下提供了一種可靠的任務排程系統,也就是在原生的線程或協程的級别上做了更高層次的封裝,這會給程式設計模式帶來巨大的好處:由于抽象了任務排程系統是以系統的線程排程可控,易于統一處理,穩定性和可維護性更高。另外開發者隻需要關心每個Actor的邏輯即可進而避免了鎖的濫用。

Actor就沒有缺點嗎?

當然不是,比如當所有邏輯都跑在Actor中的時候,很難掌握Actor的粒度,稍有不慎就可能造成系統中Actor個數爆炸的情況。另外,當必須共享資料或狀态時很難避免使用鎖,由于Actor可能會堵塞自己但Actor不應該堵塞它運作的線程,此時也許可選擇使用Redis做資料共享。

Actor模型

Actor模型是1973年提出的一個分布式并發程式設計模式,在Erlang語言中得到廣泛支援和應用。

在Actor模型中,Actor參與者是一個并發原語,簡單來說,一個參與者就是一個勞工,與程序或線程一樣能夠工作或處理任務。

可以将Actor想象成面向對象程式設計語言中的對象執行個體,不同的是Actor的狀态不能直接讀取和修改,方法也不能直接調用。Actor隻能通過消息傳遞的方式與外界通信。每個參與者存在一個代表本身的位址,但隻能向該位址發送消息。

在計算機科學領域,Actor是一個并行計算的數學模型,最初是為了由大量獨立的微處理器組成的高并行計算機所開發的。

Actor模型的理念非常簡單:萬物皆Actor

Actor模型将Actor當作通用的并行計算原語:一個參與者Actor對接收到的消息做出響應,本地政策可以建立出更多的參與者或發送更多的消息,同時準備接收下一條消息。

簡單來說,Actor模型是一個概念模型,用于處理并發計算。它定義了一系列系統元件應該如何動作和互動的通用規則,最著名的使用這套規則的程式設計語言是Erlang。

Erlang引入了”随它崩潰“的哲學理念,這部分關鍵代碼被監控着,監控者supervisor唯一的職責是知道代碼崩潰後幹什麼,讓這種理念成為可能的正是Actor模型。

在Erlang中,每段代碼都運作在程序中,程序是Erlang中對Actor的稱呼,意味着它的狀态不會影響其他程序。系統中會有一個supervisor,實際上它隻是另一個程序。被監控的程序挂掉了,supervisor會被通知并對此進行處理,是以也就能建立一個具有自愈功能的系統。如果一個Actor到達異常狀态并且崩潰,無論如何,supervisor都可以做出反應并嘗試把它變成一緻狀态,最常見的方式就是根據初始狀态重新開機Actor。

簡單來說,Actor通過消息傳遞的方式與外界通信,而且消息傳遞是異步的。每個Actor都有一個郵箱,郵箱接收并緩存其他Actor發過來的消息,通過郵箱隊列mail queue來處理消息。Actor一次隻能同步處理一個消息,處理消息過程中,除了可以接收消息外不能做任何其他操作。

每個Actor是完全獨立的,可以同時執行他們的操作。每個Actor是一個計算實體,映射接收到的消息并執行以下動作:發送有限個消息給其他Actor、建立有限個新的Actor、為下一個接收的消息指定行為。這三個動作沒有固定的順序,可以并發地執行,Actor會根據接收到的消息進行不同的處理。

在Actor系統中包含一個未處理的任務集,每個任務都由三個屬性辨別:

tag用以區分系統中的其他任務

target 通信到達的位址

communication 包含在target目标位址上的Actor,處理任務時可擷取的資訊。

為簡單起見,可見一個任務視為一個消息,在Actor之間傳遞包含以上三個屬性的值的消息。

Actor模型有兩種任務排程方式:基于線程的排程、基于事件的排程

基于線程的排程

為每個Actor配置設定一個線程,在接收一個消息時,如果目前Actor的郵箱為空則會阻塞目前線程。基于線程的排程實作較為簡單,但線程數量受到操作的限制,現在的Actor模型一般不采用這種方式。

基于事件的排程

事件可以了解為任務或消息的到來,而此時才會為Actor的任務配置設定線程并執行。

是以,可以把系統中所有事物都抽象成為一個Actor:

Actor的輸入是接收到的消息

Actor接收到消息後處理消息中定義的任務

Actor處理完成任務後可以發送消息給其它Actor

在一個系統中可以将一個大規模的任務分解為一些小任務,這些小任務可以由多個Actor并發處理,進而減少任務的完成時間。

Actor模型的另一個好處是可以消除共享狀态,因為Actor每次隻能處理一條消息,是以Actor内部可以安全的處理狀态,而不用考慮鎖機制。

Actor模型學習

Actor包含發送者和接收者,設計簡單的消息驅動對象用來實作異步性。

例如:将計數器場景中基于線程的實作替換為Actor,當然Actor也要線上程中運作,但Actor隻在有事情可做(沒有消息要處理)的時候才會使用線程。

在計數器場景中,請求者代表CutomerActor,計數器數量由TicketsActor來維護并持有目前計數器的狀态。CustomerActor和TicketsActor在空閑idle或沒有事情做的時候都不會持有線程。

在初始購買操作時CustomerActor需要發送一個消息給TicketsActor,消息中包含了要購買的數量。當TicketsActor接收到消息時會校驗購買數量是否超過庫存數量,若合法則遞減數量。此時TicketsActor會發送一條消息給CutomerActor表明訂單被成功接受。若購買數量超過庫存數量TicketsActor也會發送給CustomerActor一條消息,表明訂單被拒絕。

可劃分兩個階段的行為檢查和遞減操作,也可以通過同步操作序列來完成。但是基于Actor的實作不僅在每個Actor中提供了自然的操作同步,還能避免大量的線程積壓,防止線程等待輪到它們執行同步代碼區域。明顯會降低系統資源的占用。

Actor模型本身確定處理是按照同步的方式執行的。TicketsActor會處理其收件箱中的每條消息,注意這裡沒有複雜的線程或鎖,隻是一個多線程的處理過程,但Actor系統會管理線程的使用和配置設定。

Actor是由狀态(state)、行為(behavior)、郵箱(mailbox)三者組成的。

狀态(state):狀态是指actor對象的變量資訊,狀态由actor自身管理,避免并發環境下的鎖和記憶體原子性等問題。

行為(behavior):行為指定的是actor中計算邏輯,通過actor接收到的消息來改變actor的狀态。

郵箱(mailbox):郵箱是actor之間的通信橋梁,郵箱内部通過FIFO消息隊列來存儲發送發消息,而接收方則從郵箱中擷取消息。

Actor模型描述了一組為避免并發程式設計的公理:

所有的Actor狀态是本地的,外部是無法通路的。

Actor必須通過消息傳遞進行通信

一個Actor可以響應消息、退出新Actor、改變内部狀态、将消息發送到一個或多個Actor。

Actor可能會堵塞自己但Actor不應該堵塞自己運作的線程

Actor參與者

Actor模型學習

Actor的概念來自于Erlang,在AKKA中可以認為一個Actor就是一個容器,用來存儲狀态、行為、郵箱Mailbox、子Actor、Supervisor政策。Actor之間并不直接通信,而是通過郵件Mail來互通有無。Actor模型的本質就是消息傳遞,作為一種計算實體,Actor與原子類似。參與者是一個運算實體,回應接收到的消息,同時并行的發送有限數量的消息給其他參與者、建立有限數量的新參與者、指定接收到下一個消息時的行為。

Actor模型推崇的哲學是”一切皆是參與者“,與面向對象程式設計的”一切皆是對象“類似,但面向對象程式設計通常是順序執行的,而Actor模型則是并行執行的。一個Actor指的是一個最基本的計算單元,能夠接受一個消息并基于它執行計算。這個理念也很類似面向對象語言中:一個對象接收一個消息(方法調用),然後根據接收的消息做事兒(調用了哪個方法)。Actors一大重大特征在于actors之間互相隔離,它們并不互相共享記憶體。這點差別于上述的對象,也就是說,一個actor能維持一個私有的狀态,并且這個狀态不可能被另一個actor所改變。

在Actor模型中主角是actor,類似一種worker。Actor彼此之間直接發送消息,不需要經過什麼中介,消息是異步發送和處理的。在Actor模型中一切都是Actor,所有邏輯或子產品都可以看成是Actor,通過不同Actor之間的消息傳遞實作子產品之間的通信和互動。

Mailbox郵箱

光有一個actor是不夠的,多個actors才能組成系統。在Actor模型中每個actor都有自己的位址,是以他們才能互相發送消息。需要指明的一點是,盡管多個actors同時運作,但是一個actor隻能順序地處理消息。也就是說其它actor發送多條消息給一個actor時,這個actor隻能一次處理一條。如果需要并行的處理多條消息時,需要将消息發送給多個actor。

消息是異步的傳送到actor的,是以當actor正在處理消息時,新來的消息應該存儲到别的地方,也就是mailbox消息存儲的地方。

每個actor都有且僅有一個mailbox,mailbox相當于一個小型的隊列,一旦sender發送消息,就将該消息入隊到mailbox中。入隊的順序按照消息發送的時間順序。

Actor模型學習

異步的發送消息是用actor模型程式設計的重要特性之一,消息并不是直接發送到一個actor,而是發送到一個mailbox中的。這樣的設計解耦了actor之間的關系,每個actor都以自己的步調運作,且發送消息時不會被堵塞。雖然所有actor可以同時運作,但它們都按照mailbox接收消息的順序來依次處理消息,且僅僅在目前消息處理完畢後才會處理下一個消息,是以我們隻需要關心發送消息時的并發問題即可。

當一個actor接收到消息後,它能做如下三件事中的任意一件:

建立有限數量的新actors

發送有限數量的消息給其他參與者

指定下一條消息到來時的行為

之前說每個actor能維持一個私有狀态,”指定下一條消息到來時的行為“意味着可以定義下一條消息來到時的狀态,簡單來說,就是actors如何修改狀态。

以上操作不含有順序執行的假設,是以可以并行進行。發送者與已經發送的消息解耦,是Actor模型的根本優勢。這允許進行異步通信,同時滿足消息傳遞的控制結構。消息接收者是通過位址區分的,也就是郵件位址。是以參與者隻能和它擁有位址的參與者通信,他可以通過接收到的消息擷取位址,或者擷取它建立的參與者的位址。Actor模型的特征是,actor内部或之間進行并行計算,actor可以動态建立,actor位址包含在消息中,互動隻有通過直接的異步消息通信,不限制消息到達的順序。

作者:JunChow520

連結:https://www.jianshu.com/p/d803e2a7de8e

來源:簡書

繼續閱讀