傳統(tǒng)的游戲服務(wù)器要么是單線程要么是多線程握础,過去幾十年里CPU一直遵循摩爾定律發(fā)展脆诉,帶來的結(jié)果是單核頻率越來越高。而近幾年摩爾定義在CPU上已然失效伐坏,為什么呢怔匣?
大于在2003年左右,計算機的核心特性經(jīng)歷了一個重要的變化,處理器的速度達到了一個頂點每瞒。在接下來近15年里金闽,時鐘速度是呈線性增長的,而不會像以前那樣以指數(shù)級的速度增長剿骨。
由于CPU的工藝制程和發(fā)熱穩(wěn)定性之間難以取舍代芜,取而代之的策略是增加CPU核心的數(shù)量。多核處理器應(yīng)運而生浓利,計算處理變成了團隊協(xié)作挤庇,效率的提升通過多個核心的通信來實現(xiàn),而不是傳統(tǒng)的時鐘速度的提升贷掖。這也是線程發(fā)揮作用的地方嫡秕。
目前家用PC四核已經(jīng)非常常見,服務(wù)器更是達到32核64線程苹威。為了高效的利用多核CPU昆咽,應(yīng)該在代碼層面就考慮并發(fā)性。經(jīng)過十幾年痛苦的開發(fā)經(jīng)歷牙甫,事實告訴我們線程并不是獲取并發(fā)性的好方法掷酗,而往往會帶來難以查找的問題。
例如:以稀缺資源的計數(shù)為例窟哺,如商品的庫存數(shù)量或活動的可售門票汇在,可能存在多個請求同時獲取一個或多個商品或門票≡啻穑考慮常用實現(xiàn)方式,每個請求對應(yīng)一個線程亩鬼,很可能會有多個并發(fā)運行的線程都去調(diào)整計數(shù)器殖告。模型必須確保在同一時間只能有一個線程去遞減計數(shù)器的值。這樣做的原因是因為遞減操作存在兩個步驟:首先檢查當前計數(shù)器雳锋,確保計數(shù)器的值大于或等于要減少的值黄绩。其次遞減計數(shù)器。
為什么要將兩步操作作為一個整體操作來完成呢玷过?
因為每個請求代表購買一個或多個爽丹,假設(shè)有兩個線程并發(fā)地調(diào)整計數(shù)器,若計數(shù)器目前為10辛蚊, 線程1要想計數(shù)器遞減2粤蝎,線程2想要計數(shù)器遞減9,線程1和線程2都會檢查當前計數(shù)器的值袋马,而計數(shù)器的值均大于要遞減的數(shù)量初澎。所以線程1和線程2都會繼續(xù)運行并遞減計數(shù)器的值,最后的結(jié)果是多少呢虑凛?10-2-9=-1碑宴,問題來了软啼。這樣的結(jié)果直接操作庫存被過度分配,違反了業(yè)務(wù)規(guī)則延柠。
為了防止過度分配祸挪,原生的方式是將檢查和遞減兩步操作放到一個原子操作中,將兩步操作鎖定到一個操作中贞间,就能夠消除過度分配的可能性贿条。
例如,兩個線程同時嘗試購買最后一件商品時榜跌,如果沒有鎖就可能出現(xiàn)多個線程同時斷定計數(shù)器的值大于或等于購買數(shù)量闪唆,然后錯誤地遞減計數(shù)器,從而導(dǎo)致出現(xiàn)負數(shù)钓葫。
然而悄蕾,問題的根源在于一個請求對應(yīng)一個線程。
另外础浮,在高度競爭的階段帆调,很有可能出現(xiàn)很長的線程隊列,他們都在等待遞減計數(shù)器豆同。但使用隊列的方式的問題在于可能造成眾多阻塞線程番刊,也就是每個線程都在等待輪到它們?nèi)?zhí)行一個序列化的操作。
所以影锈,應(yīng)用設(shè)計者一不小心芹务,內(nèi)在的復(fù)雜性就有可能將多核多線程的應(yīng)用變成單線程的應(yīng)用,或者導(dǎo)致工作線程之間存在高度競爭鸭廷。
Actor模型優(yōu)雅的解決了這個難題枣抱,為真正多線程的應(yīng)用提供了一個基礎(chǔ)支持。
為什么會出現(xiàn)Actor這種并發(fā)編程的模型呢辆床?
關(guān)于這一點需要先說說并發(fā)性中的一致性和隔離性佳晶,一致性是讓數(shù)據(jù)保持一致,例如銀行轉(zhuǎn)賬的場景中讼载,轉(zhuǎn)賬完成時雙方賬戶必須是一方減少一方增加轿秧。而隔離性而可以理解為犧牲一部分一致性需求,從而獲得性能的提升咨堤。例如菇篡,在完全一致性的情況下,任務(wù)是串行的一喘,此時也就不存在隔離性了逸贾。
那為什么會有Actor模型呢?
因為傳統(tǒng)并發(fā)模式中,共享內(nèi)存是傾向于強一致性弱隔離性的铝侵,例如悲觀鎖同步的方式就是使用強一致性的方式控制并發(fā)灼伤,而Actor模型天然是強隔離性且弱一致性的,所以Actor模型在并發(fā)中有良好的性能咪鲜,而且易于控制和管理狐赡。
Actor模型的設(shè)計是消息驅(qū)動和非阻塞的,吞吐量自然也被考慮在內(nèi)疟丙。
Actor模型適用于對一致性需求不是很高且對性能需求較高的場景
綜上所述颖侄,計算機CPU的計算速度(頻率)的提高是有限的,剩下能做的是放入多個計算核心以提升性能享郊。為了利用多核心的性能览祖,需要并發(fā)執(zhí)行。但多線程的方式往往會引入很多問題炊琉,同時直接增加了調(diào)試難度展蒂。
為什么Actor模型是一種處理并發(fā)問題的解決方案呢?
處理并發(fā)問題一貫的思路是如何保證共享數(shù)據(jù)的一致性和正確性苔咪。
一般而言锰悼,有兩種策略用來在并發(fā)線程中進行通信:共享數(shù)據(jù)、消息傳遞
使用共享數(shù)據(jù)的并發(fā)編程面臨的最大問題是數(shù)據(jù)條件競爭data race
团赏,處理各種鎖的問題是讓人十分頭疼的箕般。和共享數(shù)據(jù)方式相比,消息傳遞機制最大的優(yōu)勢在于不會產(chǎn)生數(shù)據(jù)競爭狀態(tài)舔清。而實現(xiàn)消息傳遞有兩種常見類型:基于channel
的消息傳遞丝里、基于Actor
的消息傳遞。
為什么要保持共享數(shù)據(jù)的正確性呢体谒?
無非是因為程序是多線程的丙者,多個線程對同一個數(shù)據(jù)操作時若不加入同步條件,勢必造成數(shù)據(jù)污染营密。
那么為什么不能使用單線程去處理請求呢?
大部分人認為單線程處理相比多線程而言目锭,系統(tǒng)的性能將大打折扣评汰。Actor模型的出現(xiàn)解決了這些問題。
- 進程間通信
把通信的線程可以想象成兩個無法直接說話而必須通過郵件交流的人痢虹,雙方要交流就要發(fā)送郵件被去。發(fā)送方郵件一旦發(fā)出就不能修改任何內(nèi)容,而且是沒有辦法收回修改后再發(fā)的奖唯,這也就是消息一旦發(fā)出就不可改變惨缆。對于接收方而言,想什么時候看郵件就什么時候看,而且不需要監(jiān)聽坯墨,這就叫異步寂汇。接收方看了發(fā)送方的郵件可以回復(fù)也可以撒都不做。只是回復(fù)郵件一旦發(fā)出也同樣是不能收回修改的捣染,也就是不可變性兩端都是一樣的骄瓣。同樣,發(fā)送方針對回復(fù)郵件耍攘,也是想什么時候看就什么時候看榕栏。兩端同樣都是異步的。這種通信模型就是Actor想要的模型蕾各,可以發(fā)現(xiàn)這種通信方式其實依賴一套郵件系統(tǒng)或叫做消息管理系統(tǒng)扒磁。進程內(nèi)部要有一套這樣的系統(tǒng),給每個線程一個獨立的收發(fā)消息的管道式曲,并且都是異步的妨托。
- 并發(fā)性
并發(fā)導(dǎo)致最大的問題是對共享數(shù)據(jù)的操作,面對并發(fā)問題時多采用鎖去保證共享數(shù)據(jù)的一致性检访,但同樣也會帶來一系列的副作用始鱼,比如要去考慮鎖的粒度(對方法、程序塊等)脆贵、鎖的形式(讀鎖医清、寫鎖等)等問題。
傳統(tǒng)的并發(fā)編程的方式大多使用鎖機制卖氨,相信大多數(shù)都是悲觀鎖会烙,這幾乎可以斷定會出現(xiàn)兩個非常明顯的問題:隨著項目體量增大,業(yè)務(wù)愈加復(fù)雜筒捺,不可避免地會大量的使用鎖柏腻,然而鎖的機制其實是很低效的。即使大量依賴鎖解決了項目中資源競爭的情況系吭,但由于沒有一個規(guī)范的編程模式五嫂,最后系統(tǒng)的穩(wěn)定性肯定會出問題,最根本的原因是沒有把系統(tǒng)的任務(wù)調(diào)度抽象出來肯尺,由于任務(wù)調(diào)度和業(yè)務(wù)邏輯耦合在一起沃缘,很難做一個很高層的抽象以保證任務(wù)調(diào)度有序性。
Actor模型為并發(fā)而生则吟,是為解決高并發(fā)的一種編程思路槐臀。使用并發(fā)編程時需要特別關(guān)注鎖與內(nèi)存原子性等一系列的線程問題,Actor模型內(nèi)部的狀態(tài)由自身維護氓仲,也就是說Actor內(nèi)部數(shù)據(jù)只能由它自己通過消息傳遞來進行狀態(tài)修改水慨,所以使用Actor模型可以很好地避免這些問題得糜。
Actor為什么一定程度上可以解決這些問題呢?
因為Actor模型下提供了一種可靠的任務(wù)調(diào)度系統(tǒng)晰洒,也就是在原生的線程或協(xié)程的級別上做了更高層次的封裝朝抖,這會給編程模式帶來巨大的好處:由于抽象了任務(wù)調(diào)度系統(tǒng)所以系統(tǒng)的線程調(diào)度可控,易于統(tǒng)一處理欢顷,穩(wěn)定性和可維護性更高槽棍。另外開發(fā)者只需要關(guān)心每個Actor的邏輯即可從而避免了鎖的濫用。
Actor就沒有缺點嗎抬驴?
當然不是炼七,比如當所有邏輯都跑在Actor中的時候,很難掌握Actor的粒度布持,稍有不慎就可能造成系統(tǒng)中Actor個數(shù)爆炸的情況豌拙。另外,當必須共享數(shù)據(jù)或狀態(tài)時很難避免使用鎖题暖,由于Actor可能會堵塞自己但Actor不應(yīng)該堵塞它運行的線程按傅,此時也許可選擇使用Redis做數(shù)據(jù)共享。
Actor模型
Actor模型是1973年提出的一個分布式并發(fā)編程模式胧卤,在Erlang語言中得到廣泛支持和應(yīng)用唯绍。
在Actor模型中,Actor
參與者是一個并發(fā)原語枝誊,簡單來說况芒,一個參與者就是一個工人,與進程或線程一樣能夠工作或處理任務(wù)叶撒。
可以將Actor想象成面向?qū)ο缶幊陶Z言中的對象實例绝骚,不同的是Actor的狀態(tài)不能直接讀取和修改,方法也不能直接調(diào)用祠够。Actor只能通過消息傳遞的方式與外界通信压汪。每個參與者存在一個代表本身的地址,但只能向該地址發(fā)送消息古瓤。
在計算機科學(xué)領(lǐng)域止剖,Actor是一個并行計算的數(shù)學(xué)模型,最初是為了由大量獨立的微處理器組成的高并行計算機所開發(fā)的落君。
Actor模型的理念非常簡單:萬物皆Actor
Actor模型將Actor
當作通用的并行計算原語:一個參與者Actor
對接收到的消息做出響應(yīng)穿香,本地策略可以創(chuàng)建出更多的參與者或發(fā)送更多的消息,同時準備接收下一條消息叽奥。
簡單來說,Actor模型是一個概念模型痛侍,用于處理并發(fā)計算朝氓。它定義了一系列系統(tǒng)組件應(yīng)該如何動作和交互的通用規(guī)則魔市,最著名的使用這套規(guī)則的編程語言是Erlang。
Erlang引入了”隨它崩潰“的哲學(xué)理念赵哲,這部分關(guān)鍵代碼被監(jiān)控著待德,監(jiān)控者supervisor
唯一的職責(zé)是知道代碼崩潰后干什么,讓這種理念成為可能的正是Actor模型枫夺。
在Erlang中将宪,每段代碼都運行在進程中,進程是Erlang中對Actor的稱呼橡庞,意味著它的狀態(tài)不會影響其他進程较坛。系統(tǒng)中會有一個supervisor
,實際上它只是另一個進程扒最。被監(jiān)控的進程掛掉了丑勤,supervisor
會被通知并對此進行處理,因此也就能創(chuàng)建一個具有自愈功能的系統(tǒng)吧趣。如果一個Actor到達異常狀態(tài)并且崩潰法竞,無論如何,supervisor
都可以做出反應(yīng)并嘗試把它變成一致狀態(tài)强挫,最常見的方式就是根據(jù)初始狀態(tài)重啟Actor岔霸。
簡單來說,Actor通過消息傳遞的方式與外界通信俯渤,而且消息傳遞是異步的呆细。每個Actor都有一個郵箱,郵箱接收并緩存其他Actor發(fā)過來的消息稠诲,通過郵箱隊列mail queue
來處理消息侦鹏。Actor一次只能同步處理一個消息,處理消息過程中臀叙,除了可以接收消息外不能做任何其他操作略水。
每個Actor是完全獨立的,可以同時執(zhí)行他們的操作劝萤。每個Actor是一個計算實體渊涝,映射接收到的消息并執(zhí)行以下動作:發(fā)送有限個消息給其他Actor、創(chuàng)建有限個新的Actor床嫌、為下一個接收的消息指定行為跨释。這三個動作沒有固定的順序,可以并發(fā)地執(zhí)行厌处,Actor會根據(jù)接收到的消息進行不同的處理鳖谈。
在Actor系統(tǒng)中包含一個未處理的任務(wù)集,每個任務(wù)都由三個屬性標識:
-
tag
用以區(qū)分系統(tǒng)中的其他任務(wù) -
target
通信到達的地址 -
communication
包含在target
目標地址上的Actor阔涉,處理任務(wù)時可獲取的信息缆娃。
為簡單起見捷绒,可見一個任務(wù)視為一個消息,在Actor之間傳遞包含以上三個屬性的值的消息贯要。
Actor模型有兩種任務(wù)調(diào)度方式:基于線程的調(diào)度暖侨、基于事件的調(diào)度
- 基于線程的調(diào)度
為每個Actor分配一個線程,在接收一個消息時崇渗,如果當前Actor的郵箱為空則會阻塞當前線程字逗。基于線程的調(diào)度實現(xiàn)較為簡單宅广,但線程數(shù)量受到操作的限制葫掉,現(xiàn)在的Actor模型一般不采用這種方式。 - 基于事件的調(diào)度
事件可以理解為任務(wù)或消息的到來乘碑,而此時才會為Actor的任務(wù)分配線程并執(zhí)行挖息。
因此,可以把系統(tǒng)中所有事物都抽象成為一個Actor:
- Actor的輸入是接收到的消息
- Actor接收到消息后處理消息中定義的任務(wù)
- Actor處理完成任務(wù)后可以發(fā)送消息給其它Actor
在一個系統(tǒng)中可以將一個大規(guī)模的任務(wù)分解為一些小任務(wù)兽肤,這些小任務(wù)可以由多個Actor并發(fā)處理套腹,從而減少任務(wù)的完成時間。
Actor模型的另一個好處是可以消除共享狀態(tài)资铡,因為Actor每次只能處理一條消息电禀,所以Actor內(nèi)部可以安全的處理狀態(tài),而不用考慮鎖機制笤休。
Actor
包含發(fā)送者和接收者尖飞,設(shè)計簡單的消息驅(qū)動對象用來實現(xiàn)異步性。
例如:將計數(shù)器場景中基于線程的實現(xiàn)替換為Actor
店雅,當然Actor
也要在線程中運行政基,但Actor
只在有事情可做(沒有消息要處理)的時候才會使用線程。
在計數(shù)器場景中闹啦,請求者代表CutomerActor
沮明,計數(shù)器數(shù)量由TicketsActor
來維護并持有當前計數(shù)器的狀態(tài)。CustomerActor
和TicketsActor
在空閑idle
或沒有事情做的時候都不會持有線程窍奋。
在初始購買操作時CustomerActor
需要發(fā)送一個消息給TicketsActor
荐健,消息中包含了要購買的數(shù)量。當TicketsActor
接收到消息時會校驗購買數(shù)量是否超過庫存數(shù)量琳袄,若合法則遞減數(shù)量江场。此時TicketsActor
會發(fā)送一條消息給CutomerActor
表明訂單被成功接受。若購買數(shù)量超過庫存數(shù)量TicketsActor
也會發(fā)送給CustomerActor
一條消息窖逗,表明訂單被拒絕址否。
可劃分兩個階段的行為檢查和遞減操作,也可以通過同步操作序列來完成碎紊。但是基于Actor
的實現(xiàn)不僅在每個Actor
中提供了自然的操作同步佑附,還能避免大量的線程積壓用含,防止線程等待輪到它們執(zhí)行同步代碼區(qū)域。明顯會降低系統(tǒng)資源的占用帮匾。
Actor
模型本身確保處理是按照同步的方式執(zhí)行的。TicketsActor
會處理其收件箱中的每條消息痴鳄,注意這里沒有復(fù)雜的線程或鎖瘟斜,只是一個多線程的處理過程,但Actor
系統(tǒng)會管理線程的使用和分配痪寻。
Actor是由狀態(tài)(state)螺句、行為(behavior)、郵箱(mailbox)三者組成的橡类。
- 狀態(tài)(state):狀態(tài)是指actor對象的變量信息蛇尚,狀態(tài)由actor自身管理,避免并發(fā)環(huán)境下的鎖和內(nèi)存原子性等問題顾画。
- 行為(behavior):行為指定的是actor中計算邏輯取劫,通過actor接收到的消息來改變actor的狀態(tài)。
- 郵箱(mailbox):郵箱是actor之間的通信橋梁研侣,郵箱內(nèi)部通過FIFO消息隊列來存儲發(fā)送發(fā)消息谱邪,而接收方則從郵箱中獲取消息。
Actor模型描述了一組為避免并發(fā)編程的公理:
- 所有的Actor狀態(tài)是本地的庶诡,外部是無法訪問的惦银。
- Actor必須通過消息傳遞進行通信
- 一個Actor可以響應(yīng)消息、退出新Actor末誓、改變內(nèi)部狀態(tài)扯俱、將消息發(fā)送到一個或多個Actor。
- Actor可能會堵塞自己但Actor不應(yīng)該堵塞自己運行的線程
Actor參與者
Actor的概念來自于Erlang喇澡,在AKKA中可以認為一個Actor就是一個容器迅栅,用來存儲狀態(tài)、行為撩幽、郵箱Mailbox库继、子Actor、Supervisor策略窜醉。Actor之間并不直接通信宪萄,而是通過郵件Mail來互通有無。Actor模型的本質(zhì)就是消息傳遞榨惰,作為一種計算實體拜英,Actor與原子類似。參與者是一個運算實體琅催,回應(yīng)接收到的消息居凶,同時并行的發(fā)送有限數(shù)量的消息給其他參與者虫给、創(chuàng)建有限數(shù)量的新參與者、指定接收到下一個消息時的行為侠碧。
Actor模型推崇的哲學(xué)是”一切皆是參與者“抹估,與面向?qū)ο缶幊痰摹币磺薪允菍ο蟆邦愃疲嫦驅(qū)ο缶幊掏ǔJ琼樞驁?zhí)行的弄兜,而Actor模型則是并行執(zhí)行的药蜻。一個Actor指的是一個最基本的計算單元,能夠接受一個消息并基于它執(zhí)行計算替饿。這個理念也很類似面向?qū)ο笳Z言中:一個對象接收一個消息(方法調(diào)用)语泽,然后根據(jù)接收的消息做事兒(調(diào)用了哪個方法)。Actors一大重大特征在于actors之間相互隔離视卢,它們并不相互共享內(nèi)存踱卵。這點區(qū)別于上述的對象,也就是說据过,一個actor能維持一個私有的狀態(tài)惋砂,并且這個狀態(tài)不可能被另一個actor所改變。
在Actor模型中主角是actor绳锅,類似一種worker班利。Actor彼此之間直接發(fā)送消息,不需要經(jīng)過什么中介榨呆,消息是異步發(fā)送和處理的罗标。在Actor模型中一切都是Actor,所有邏輯或模塊都可以看成是Actor积蜻,通過不同Actor之間的消息傳遞實現(xiàn)模塊之間的通信和交互闯割。
Mailbox郵箱
光有一個actor是不夠的,多個actors才能組成系統(tǒng)竿拆。在Actor模型中每個actor都有自己的地址宙拉,所以他們才能相互發(fā)送消息。需要指明的一點是丙笋,盡管多個actors同時運行谢澈,但是一個actor只能順序地處理消息。也就是說其它actor發(fā)送多條消息給一個actor時御板,這個actor只能一次處理一條锥忿。如果需要并行的處理多條消息時,需要將消息發(fā)送給多個actor怠肋。
消息是異步的傳送到actor的敬鬓,所以當actor正在處理消息時,新來的消息應(yīng)該存儲到別的地方,也就是mailbox消息存儲的地方钉答。
每個actor都有且僅有一個mailbox础芍,mailbox相當于一個小型的隊列,一旦sender發(fā)送消息数尿,就將該消息入隊到mailbox中仑性。入隊的順序按照消息發(fā)送的時間順序。
異步的發(fā)送消息是用actor模型編程的重要特性之一右蹦,消息并不是直接發(fā)送到一個actor虏缸,而是發(fā)送到一個mailbox中的梯啤。這樣的設(shè)計解耦了actor之間的關(guān)系攒至,每個actor都以自己的步調(diào)運行啼器,且發(fā)送消息時不會被堵塞。雖然所有actor可以同時運行甲献,但它們都按照mailbox接收消息的順序來依次處理消息,且僅僅在當前消息處理完畢后才會處理下一個消息颂翼,因此我們只需要關(guān)心發(fā)送消息時的并發(fā)問題即可晃洒。
當一個actor接收到消息后,它能做如下三件事中的任意一件:
- 創(chuàng)建有限數(shù)量的新actors
- 發(fā)送有限數(shù)量的消息給其他參與者
- 指定下一條消息到來時的行為
之前說每個actor能維持一個私有狀態(tài)朦乏,”指定下一條消息到來時的行為“意味著可以定義下一條消息來到時的狀態(tài)球及,簡單來說,就是actors如何修改狀態(tài)呻疹。
以上操作不含有順序執(zhí)行的假設(shè)吃引,因此可以并行進行。發(fā)送者與已經(jīng)發(fā)送的消息解耦刽锤,是Actor模型的根本優(yōu)勢镊尺。這允許進行異步通信,同時滿足消息傳遞的控制結(jié)構(gòu)并思。消息接收者是通過地址區(qū)分的庐氮,也就是郵件地址。因此參與者只能和它擁有地址的參與者通信宋彼,他可以通過接收到的消息獲取地址弄砍,或者獲取它創(chuàng)建的參與者的地址。Actor模型的特征是输涕,actor內(nèi)部或之間進行并行計算音婶,actor可以動態(tài)創(chuàng)建,actor地址包含在消息中莱坎,交互只有通過直接的異步消息通信桃熄,不限制消息到達的順序。
最佳實踐
素數(shù)計算
需求:使用多線程找出1000000以內(nèi)素數(shù)個數(shù)
傳統(tǒng)方式通過鎖/同步的方式實現(xiàn)并發(fā),每次同步獲取當前值并讓一個線程去判斷值是否為素數(shù)瞳收,若是的話則通過同步方式對計數(shù)器加一碉京。
使用Actor模型方式會將此過程拆分成多個模塊,即拆分成多個Actor螟深。每個Actor負責(zé)不同部分谐宙,并通過消息傳遞讓多個Actor協(xié)同工作。
銀行轉(zhuǎn)賬
存在的問題:當用戶A Actor扣款期間界弧,用戶B Actor是不受限的凡蜻,此時對用戶B Actor進行操作是合法的,針對這種情況垢箕,單純的Actor模型就顯得比較乏力划栓,需要加入其他機制來保證一致性。