轉自:
鏈接:https://www.jianshu.com/p/d803e2a7de8e
來源:簡書
傳統的游戲服務器要么是單線程要么是多線程,過去幾十年里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
只在有事情可做(沒有消息要處理)的時候才會使用線程。
在計數器場景中,請求者代表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的概念來自於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,而是發送到一個mailbox中的。這樣的設計解耦了actor之間的關系,每個actor都以自己的步調運行,且發送消息時不會被堵塞。雖然所有actor可以同時運行,但它們都按照mailbox接收消息的順序來依次處理消息,且僅僅在當前消息處理完畢后才會處理下一個消息,因此我們只需要關心發送消息時的並發問題即可。
當一個actor接收到消息后,它能做如下三件事中的任意一件:
- 創建有限數量的新actors
- 發送有限數量的消息給其他參與者
- 指定下一條消息到來時的行為
之前說每個actor能維持一個私有狀態,”指定下一條消息到來時的行為“意味着可以定義下一條消息來到時的狀態,簡單來說,就是actors如何修改狀態。
以上操作不含有順序執行的假設,因此可以並行進行。發送者與已經發送的消息解耦,是Actor模型的根本優勢。這允許進行異步通信,同時滿足消息傳遞的控制結構。消息接收者是通過地址區分的,也就是郵件地址。因此參與者只能和它擁有地址的參與者通信,他可以通過接收到的消息獲取地址,或者獲取它創建的參與者的地址。Actor模型的特征是,actor內部或之間進行並行計算,actor可以動態創建,actor地址包含在消息中,交互只有通過直接的異步消息通信,不限制消息到達的順序。
最佳實踐
素數計算
需求:使用多線程找出1000000以內素數個數

傳統方式通過鎖/同步的方式實現並發,每次同步獲取當前值並讓一個線程去判斷值是否為素數,若是的話則通過同步方式對計數器加一。

使用Actor模型方式會將此過程拆分成多個模塊,即拆分成多個Actor。每個Actor負責不同部分,並通過消息傳遞讓多個Actor協同工作。
銀行轉賬

存在的問題:當用戶A Actor扣款期間,用戶B Actor是不受限的,此時對用戶B Actor進行操作是合法的,針對這種情況,單純的Actor模型就顯得比較乏力,需要加入其他機制來保證一致性。