參考
碼農(nóng)翻身 當多線程并發(fā)遇到Actor
goroutine, channel 和 CSP
并發(fā)之痛 Thread蝙寨,Goroutine晒衩,Actor
一、多線程并發(fā)的難題
張大胖在做一個銀行相關(guān)的項目籽慢,寫了一個Account的類浸遗,用來表示一個用戶的銀行賬號,根據(jù)銀行的常規(guī)業(yè)務(wù)箱亿,自然要提供兩個方法跛锌,存款(deposit)和取款(withdraw)。
為了防止多線程并發(fā)時導致的數(shù)據(jù)不一致問題届惋,張大胖給每個方法都加了synchronized髓帽, 那意思很清楚,想進入某個方法執(zhí)行存款或取款操作脑豹,必須得先獲得一把鎖才行郑藏。
但是在做轉(zhuǎn)賬操作的時候必盖,為了保證一致性,必須得把兩個賬戶都加上鎖俱饿,然后才可以操作歌粥,于是張大胖寫下了這樣的代碼,他覺得很簡單拍埠,立刻就提交給Bill 失驶,讓他Review。
富有經(jīng)驗的Bill立刻就發(fā)現(xiàn)了問題枣购,馬上對張大胖說:“這樣會出現(xiàn)死鎖嬉探!”
張大胖說:“這么簡單的代碼,怎么可能有死鎖棉圈?”
“假設(shè)線程1 做的操作是賬戶A給賬戶B轉(zhuǎn)賬涩堤, 先鎖住了A賬戶, 接下來試圖申請B賬戶的鎖分瘾;與此同時線程2 在從 賬戶B給賬戶A 轉(zhuǎn)賬定躏, 先鎖住了B賬戶的鎖, 接下來試圖申請A賬戶的鎖。兩個線程各自持有資源痊远, 然后等待獲取對方的資源垮抗, 都無法執(zhí)行下去, 死鎖就出現(xiàn)了碧聪!”
張大胖無言以對冒版,不得不承認Bill是正確的。他問道:“那怎么解決這個問題逞姿?”
“非常簡單辞嗡,加鎖的時候按次序來就可以了,例如所有的線程滞造,無論是從A向B轉(zhuǎn)賬续室,還是從B向A轉(zhuǎn)賬,都先獲得賬號A的鎖谒养,成功后再獲得賬戶B的鎖挺狰,這樣就沒問題了÷蚩撸”
張大胖說:“那樣代碼會變得很古怪啊丰泊,還得給兩個賬戶排個順序,如果不知道背后的思想讀起來很痛苦始绍,怪不得人家說多線程編程很難啊瞳购。”
Bill說:“是啊亏推, 其實線程這個東西学赛,就是一段代碼的執(zhí)行而已, 是操作系統(tǒng)層面的概念吞杭,可是我們苦逼的程序員不得不來面對它盏浇,來背這個多線程并發(fā)的鍋了∑ぃ”
二缠捌、黑盒子
下班后锄贷,張大胖一直在思考這個問題:既然線程是操作系統(tǒng)層面的概念译蒂,能不能把線程的概念隱藏起來,然后所有的操作都不用加鎖呢谊却? 這樣以來編程就會容易得多叭嶂纭!
本質(zhì)的問題是什么炎辨?
首先是共享的狀態(tài)捕透,例如Account中的balance ,多個線程都要讀寫, 其次就是多個線程亂序乙嘀、并發(fā)執(zhí)行末购。
能不能換個思路,把這個Account對象看成一個黑盒子虎谢,你想存款了盟榴,就發(fā)一個存款的消息過來,想取款就發(fā)一個取款的消息過來婴噩。
不管是有一個消息擎场,還是有100個消息,我統(tǒng)統(tǒng)放到黑盒子的一個隊例中几莽,然后讓Account對象一個個順序處理不就可以了迅办? 根本不用在方法上加鎖!
這樣做章蚣,其實就是把并發(fā)的操作變成了串行的操作而已站欺!
不對,如果調(diào)用方把取款消息放下就走究驴,不等待返回結(jié)果镊绪,那就不是同步操作,而是異步操作了洒忧!但是如果取款的時候發(fā)現(xiàn)余額不足蝴韭,怎么通知調(diào)用方?嗯熙侍,調(diào)用方也必須是個黑盒子對象榄鉴,也向它發(fā)送異步消息,這個消息也會在消息隊列中存下來蛉抓,調(diào)用方“黑盒子”也會一個個處理庆尘。
想到這一層,張大胖激動起來:取款和存款的操作就不用在加鎖了巷送,碼農(nóng)們只要考慮黑盒子對消息的處理即可:取出消息驶忌,處理消息,向別的黑盒子發(fā)送消息笑跛, 根本不用考慮線程這樣底層的概念了付魔。
三、Actor模型
第二天張大胖趕緊找到Bill, 向他炫耀自己的“新發(fā)明”飞蹂。
Bill不動聲色:“小伙子几苍,不錯啊,重新發(fā)明了輪子陈哑!”
“重新發(fā)明妻坝?”
“是啊伸眶,你這個所謂黑盒子,就是所謂Actor模型肮粝堋厘贼! 它最早由Carl Hewitt在1973定義,其消息傳遞的方式更加符合面向?qū)ο蟮脑家鈭D圣拄, 這一點我想你也體會到了涂臣,要不你怎么把他們叫做黑盒子啊∈鄣#”
“1973年赁遗? 我還沒出生。唉族铆,看來這些概念已經(jīng)被老前輩們都發(fā)明完了啊岩四。”
“Actor屬于并發(fā)組件模型 哥攘,可以把程序員從多線程并發(fā)或線程池等基礎(chǔ)概念中解放出來剖煌。它有這么幾個特點:”
- Actor:就是你說的黑盒子,系統(tǒng)是由很多Actor組成逝淹。 Actor之間不共享狀態(tài)耕姊,但是會接收別的Actor發(fā)送的異步消息,處理的過程中栅葡,會改變內(nèi)部狀態(tài)茉兰,也可能向別的Actor發(fā)送消息。
- Message:消息是不可變的欣簇, 它的發(fā)送都是異步的规脸,Actor內(nèi)部有個“MailBox”來緩存消息。
- MailBox:Actor內(nèi)部緩存消息的郵箱熊咽, 其他Actor發(fā)送的消息都放到這里莫鸭,然后被本Actor處理,類似有多個生成者和一個消費者的隊例横殴。
張大胖說:“和我之前的圖差不多被因,看來我確實是重新發(fā)明了輪子啊∩缆兀”
四梨与、用Actor實現(xiàn)轉(zhuǎn)賬
Bill 笑道:“這個Actor看起來很美,但是編程的時候你得刷新一下你的思維才行惑畴。 大胖蛋欣,之前你的轉(zhuǎn)賬操作在多線程下不是會出現(xiàn)死鎖嗎航徙? 你考慮下如贷,如果用Actor的思路該怎么寫陷虎?”
“首先,得有兩個Actor, 這兩個Actor 表示了兩個賬戶杠袱,我把它們叫做旺財和小強尚猿。”
“然后呢楣富,轉(zhuǎn)賬的邏輯怎么處理凿掂?”
張大胖想了一會:“既然轉(zhuǎn)賬是在兩個Actor之間發(fā)生的,那可以引入一個協(xié)調(diào)者Actor纹蝴,叫做轉(zhuǎn)賬管家吧庄萎。不過,由于消息都是異步的塘安,轉(zhuǎn)賬管家向旺財這個Actor發(fā)起扣款請求以后糠涛,不知道什么時候才能真正執(zhí)行扣款,也不能立刻知道是否成功兼犯,必須得等待啊忍捡,這就有點麻煩了∏星”
Bill說:“我給你畫個流程圖砸脊,你看看∥诚迹”
張大胖感慨地說:“原來的多線程并發(fā)模型凌埂,需要同時鎖住兩個賬戶,然后才能進行轉(zhuǎn)賬∈撸現(xiàn)在每個Actor都獨立侨舆,也把這個轉(zhuǎn)賬給搞定了【钅埃”
Bill說:“其實對于轉(zhuǎn)賬管家來說挨下,對每個轉(zhuǎn)賬的消息,內(nèi)部是隱含一個流程狀態(tài)的脐湾,就是先向某個賬戶扣款臭笆,成功以后再向另一個賬戶增加,最后給調(diào)用者返回狀態(tài)秤掌,這個次序是不能亂的愁铺。看到圖中那個Transaction ID沒有(Tx01)闻鉴,就是用來跟蹤這個轉(zhuǎn)賬的事務(wù)茵乱。”
五孟岛、漏洞
“我發(fā)現(xiàn)了一個漏洞瓶竭,你這個轉(zhuǎn)賬雖然看起來很美督勺,沒有加鎖,但是和原來的是有區(qū)別的斤贰,原來多線程思路是會把旺財和小強的賬戶同時鎖住智哀,然后轉(zhuǎn)賬,在這個過程中荧恍,別人是不能操作這兩個賬號的瓷叫! 而你的Actor方案中,當轉(zhuǎn)賬管家給旺財發(fā)消息扣款的時候送巡,小強其實是自由的摹菠,如果這時候小強的賬戶被凍結(jié),那你的轉(zhuǎn)賬管家還得回滾旺財?shù)目劭钇@多麻煩啊辨嗽。”
Bill:“哈哈淮腾,你小子還挺機靈的嘛糟需,看出了這個問題,Actor模型非常適用于多個組件獨立工作谷朝,相互之間僅僅依靠消息傳遞的情況洲押。如果想在多個組件之間維持一致的狀態(tài)(比如咱們例子中的轉(zhuǎn)賬),那就不爽了圆凰¤菊剩”
“那怎么解決這個問題?”
“那必須得用一些特殊手段了专钉,有些實現(xiàn)Actor的框架挑童,例如Akka,專門提供了像Coordinated /Transactor這樣的機制來處理這個問題跃须。有空的話給你仔細講講站叼。”
“好吧菇民,我回頭看看這個Akka尽楔, 對了, Actor雖然對用戶隱藏了線程第练, 但是總得有線程來處理消息吧阔馋。” 張大胖問道娇掏。
“那是肯定的呕寝,線程本質(zhì)上就是一段代碼的執(zhí)行,每個Actor在處理消息的時候婴梧,肯定得和線程關(guān)聯(lián)才行下梢,只不過Actor系統(tǒng)把線程這個概念給隱藏了客蹋。”
“有哪些系統(tǒng)實現(xiàn)了Actor?” 張大胖接著問怔球。
“其實最著名的就是Erlang了,Actor模型可以說是它的基礎(chǔ)浮还,除了我們上面所說的竟坛,還可以讓Actor之間建立關(guān)聯(lián),例如讓一個Actor去監(jiān)控另外一些Actor工作钧舌,如果那些Actor崩潰了担汤,就新建一個Actor繼續(xù)工作。在Java 領(lǐng)域洼冻,剛才提到的Akka是比較知名的一個Actor框架崭歧。 ”
六、解決方案
1.線程池方案
Java1.5后撞牢,Doug Lea的Executor系列被包含在默認的JDK內(nèi)率碾,是典型的線程池方案。
線程池一定程度上控制了線程的數(shù)量屋彪,實現(xiàn)了線程復用所宰,降低了線程的使用成本。但還是沒有解決數(shù)量的問題畜挥,線程池初始化的時候還是要設(shè)置一個最小和最大線程數(shù)仔粥,以及任務(wù)隊列的長度,自管理只是在設(shè)定范圍內(nèi)的動態(tài)調(diào)整蟹但。另外不同的任務(wù)可能有不同的并發(fā)需求躯泰,為了避免互相影響可能需要多個線程池,最后導致的結(jié)果就是Java的系統(tǒng)里充斥了大量的線程池华糖。
2.新的思路
從前面的分析我們可以看出麦向,如果線程是一直處于運行狀態(tài),我們只需設(shè)置和CPU核數(shù)相等的線程數(shù)即可客叉,這樣就可以最大化的利用CPU磕蛇,并且降低切換成本以及內(nèi)存使用。但如何做到這一點呢十办?
陳力就列秀撇,不能者止
這句話是說,能干活的代碼片段就放在線程里向族,如果干不了活(需要等待呵燕,被阻塞等),就摘下來件相。通俗的說就是不要占著茅坑不拉屎再扭,如果拉不出來氧苍,需要醞釀下,先把茅坑讓出來泛范,因為茅坑是稀缺資源让虐。
要做到這點一般有兩種方案:
異步回調(diào)方案
典型如NodeJS,遇到阻塞的情況罢荡,比如網(wǎng)絡(luò)調(diào)用赡突,則注冊一個回調(diào)方法(其實還包括了一些上下文數(shù)據(jù)對象)給IO調(diào)度器(linux下是libev,調(diào)度器在另外的線程里)区赵,當前線程就被釋放了惭缰,去干別的事情了。等數(shù)據(jù)準備好笼才,調(diào)度器會將結(jié)果傳遞給回調(diào)方法然后執(zhí)行漱受,執(zhí)行其實不在原來發(fā)起請求的線程里了,但對用戶來說無感知骡送。但這種方式的問題就是很容易遇到callback hell昂羡,因為所有的阻塞操作都必須異步,否則系統(tǒng)就卡死了摔踱。還有就是異步的方式有點違反人類思維習慣紧憾,人類還是習慣同步的方式。GreenThread/Coroutine/Fiber方案
這種方案其實和上面的方案本質(zhì)上區(qū)別不大昌渤,關(guān)鍵在于回調(diào)上下文的保存以及執(zhí)行機制赴穗。為了解決回調(diào)方法帶來的難題,這種方案的思路是寫代碼的時候還是按順序?qū)懓蛳ⅲ龅絀O等阻塞調(diào)用時般眉,將當前的代碼片段暫停,保存上下文潜支,讓出當前線程甸赃。等IO事件回來,然后再找個線程讓當前代碼片段恢復上下文繼續(xù)執(zhí)行冗酿,寫代碼的時候感覺好像是同步的埠对,仿佛在同一個線程完成的,但實際上系統(tǒng)可能切換了線程裁替,但對程序無感项玛。
GreenThread
- 用戶空間 首先是在用戶空間,避免內(nèi)核態(tài)和用戶態(tài)的切換導致的成本弱判。
- 由語言或者框架層調(diào)度
- 更小的椊缶冢空間允許創(chuàng)建大量實例(百萬級別)
幾個概念
- Continuation 這個概念不熟悉FP編程的人可能不太熟悉,不過這里可以簡單的顧名思義,可以理解為讓我們的程序可以暫停开伏,然后下次調(diào)用繼續(xù)(contine)從上次暫停的地方開始的一種機制。相當于程序調(diào)用多了一種入口固灵。
- Coroutine 是Continuation的一種實現(xiàn),一般表現(xiàn)為語言層面的組件或者類庫丛忆。主要提供yield大审,resume機制座哩。
- Fiber 和Coroutine其實是一體兩面的徒扶,主要是從系統(tǒng)層面描述,可以理解成Coroutine運行之后的東西就是Fiber根穷。
3.Goroutine
Goroutine其實就是前面GreenThread系列解決方案的一種演進和實現(xiàn)姜骡。
- 首先,它內(nèi)置了Coroutine機制屿良。因為要用戶態(tài)的調(diào)度圈澈,必須有可以讓代碼片段可以暫停/繼續(xù)的機制。
- 其次尘惧,它內(nèi)置了一個調(diào)度器康栈,實現(xiàn)了Coroutine的多線程并行調(diào)度,同時通過對網(wǎng)絡(luò)等庫的封裝喷橙,對用戶屏蔽了調(diào)度細節(jié)啥么。
- 最后,提供了Channel機制贰逾,用于Goroutine之間通信悬荣,實現(xiàn)CSP并發(fā)模型(Communicating Sequential Processes)。因為Go的Channel是通過語法關(guān)鍵詞提供的疙剑,對用戶屏蔽了許多細節(jié)氯迂。其實Go的Channel和Java中的SynchronousQueue是一樣的機制,如果有buffer其實就是ArrayBlockQueue言缤。
這個圖一般講Goroutine調(diào)度器的地方都會引用嚼蚀,想要仔細了解的可以看看原博客。這里只說明幾點:
- M代表系統(tǒng)線程管挟,P代表處理器(核)驰坊,G代表Goroutine。Go實現(xiàn)了M:N的調(diào)度,也就是說線程和Goroutine之間是多對多的關(guān)系拳芙。這點在許多GreenThread/Coroutine的調(diào)度器并沒有實現(xiàn)察藐。比如Java1.1版本之前的線程其實是GreenThread(這個詞就來源于Java),但由于沒實現(xiàn)多對多的調(diào)度舟扎,也就是沒有真正實現(xiàn)并行分飞,發(fā)揮不了多核的優(yōu)勢譬猫,所以后來改成基于系統(tǒng)內(nèi)核的Thread實現(xiàn)了染服。
- 某個系統(tǒng)線程如果被阻塞,排列在該線程上的Goroutine會被遷移秉颗。當然還有其他機制蚕甥,比如M空閑了菇怀,如果全局隊列沒有任務(wù)爱沟,可能會從其他M偷任務(wù)執(zhí)行钥顽,相當于一種rebalance機制蜂大。這里不再細說奶浦,有需要看專門的分析文章澳叉。
- 具體的實現(xiàn)策略和我們前面分析的機制類似五督。系統(tǒng)啟動時充包,會啟動一個獨立的后臺線程(不在Goroutine的調(diào)度線程池里)基矮,啟動netpoll的輪詢家浇。當有Goroutine發(fā)起網(wǎng)絡(luò)請求時钢悲,網(wǎng)絡(luò)庫會將fd(文件描述符)和pollDesc(用于描述netpoll的結(jié)構(gòu)體譬巫,包含因為讀/寫這個fd而阻塞的Goroutine)關(guān)聯(lián)起來督笆,然后調(diào)用runtime.gopark方法,掛起當前的Goroutine荠诬。當后臺的netpoll輪詢獲取到epoll(linux環(huán)境下)的event晒杈,會將event中的pollDesc取出來拯钻,找到關(guān)聯(lián)的阻塞Goroutine粪般,并進行恢復亩歹。
4.Goroutine是銀彈么亭姥?
Goroutine很大程度上降低了并發(fā)的開發(fā)成本致份,是不是我們所有需要并發(fā)的地方直接go func就搞定了呢氮块?
Go通過Goroutine的調(diào)度解決了CPU利用率的問題滔蝉。但遇到其他的瓶頸資源如何處理蝠引?比如帶鎖的共享資源螃概,比如數(shù)據(jù)庫連接等吊洼。互聯(lián)網(wǎng)在線應(yīng)用場景下豺鼻,如果每個請求都扔到一個Goroutine里,當資源出現(xiàn)瓶頸的時候谬莹,會導致大量的Goroutine阻塞附帽,最后用戶請求超時。這時候就需要用Goroutine池來進行控流慢显,同時問題又來了:池子里設(shè)置多少個Goroutine合適?
所以這個問題還是沒有從根本上解決屋灌。
七、Actor模型
Actor對沒接觸過這個概念的人可能不太好理解除嘹,Actor的概念其實和OO里的對象類似尉咕,是一種抽象年缎。面對對象編程對現(xiàn)實的抽象是對象=屬性+行為(method),但當使用方調(diào)用對象行為(method)的時候洲鸠,其實占用的是調(diào)用方的CPU時間片坛怪,是否并發(fā)也是由調(diào)用方?jīng)Q定的。這個抽象其實和現(xiàn)實世界是有差異的≈烧睿現(xiàn)實世界更像Actor的抽象内狗,互相都是通過異步消息通信的。比如你對一個美女say hi赂鲤,美女是否回應(yīng),如何回應(yīng)是由美女自己決定的找爱,運行在美女自己的大腦里,并不會占用發(fā)送者的大腦吮播。
所以Actor有以下特征:
- Processing – actor可以做計算的薄料,不需要占用調(diào)用方的CPU時間片,并發(fā)策略也是由自己決定获列。
- Storage – actor可以保存狀態(tài)
- Communication – actor之間可以通過發(fā)送消息通訊
Actor遵循以下規(guī)則:
- 發(fā)送消息給其他的Actor
- 創(chuàng)建其他的Actor
- 接受并處理消息迫悠,修改自己的狀態(tài)
Actor的目標:
- Actor可獨立更新,實現(xiàn)熱升級鞠抑。因為Actor互相之間沒有直接的耦合,是相對獨立的實體箕速,可能實現(xiàn)熱升級。
- 無縫彌合本地和遠程調(diào)用 因為Actor使用基于消息的通訊機制字柠,無論是和本地的Actor募谎,還是遠程Actor交互节槐,都是通過消息,這樣就彌合了本地和遠程的差異揍庄。
- 容錯 Actor之間的通信是異步的,發(fā)送方只管發(fā)送缭黔,不關(guān)心超時以及錯誤食茎,這些都由框架層和獨立的錯誤處理機制接管。
- 易擴展馏谨,天然分布式 因為Actor的通信機制彌合了本地和遠程調(diào)用别渔,本地Actor處理不過來的時候,可以在遠程節(jié)點上啟動Actor然后轉(zhuǎn)發(fā)消息過去惧互。
Actor的實現(xiàn):
- Erlang/OTP Actor模型的標桿哎媚,其他的實現(xiàn)基本上都一定程度參照了Erlang的模式。實現(xiàn)了熱升級以及分布式喊儡。
- Akka(Scala,Java)基于線程和異步回調(diào)模式實現(xiàn)拨与。由于Java中沒有Fiber,所以是基于線程的荚斯。為了避免線程被阻塞,Akka中所有的阻塞操作都需要異步化。要么是Akka提供的異步框架,要么通過Future-callback機制熙暴,轉(zhuǎn)換成回調(diào)模式唉匾。實現(xiàn)了分布式峡懈,但還不支持熱升級食寡。
- Quasar (Java) 為了解決Akka的阻塞回調(diào)問題,Quasar通過字節(jié)碼增強的方式咒循,在Java中實現(xiàn)了Coroutine/Fiber蚁署。同時通過ClassLoader的機制實現(xiàn)了熱升級。缺點是系統(tǒng)啟動的時候要通過javaagent機制進行字節(jié)碼增強。
八梗夸、Golang CSP VS Actor
二者的格言都是:
Don’t communicate by sharing memory, share memory by communicating
通過消息通信的機制來避免競態(tài)條件父虑,但具體的抽象和實現(xiàn)上有些差異。
- CSP模型里消息和Channel是主體括细,處理器是匿名的。
也就是說發(fā)送方需要關(guān)心自己的消息類型以及應(yīng)該寫到哪個Channel,但不需要關(guān)心誰消費了它洞就,以及有多少個消費者。Channel一般都是類型綁定的挠羔,一個Channel只寫同一種類型的消息锭环,所以CSP需要支持alt/select機制三痰,同時監(jiān)聽多個Channel。Channel是同步的模式(Golang的Channel支持buffer常熙,支持一定數(shù)量的異步)纽竣,背后的邏輯是發(fā)送方非常關(guān)心消息是否被處理刀荒,CSP要保證每個消息都被正常處理了柴罐,沒被處理就阻塞著。 - Actor模型里Actor是主體党瓮,Mailbox(類似于CSP的Channel)是透明的详炬。
也就是說它假定發(fā)送方會關(guān)心消息發(fā)給誰消費了,但不關(guān)心消息類型以及通道寞奸。所以Mailbox是異步模式呛谜,發(fā)送者不能假定發(fā)送的消息一定被收到和處理。Actor模型必須支持強大的模式匹配機制蝇闭,因為無論什么類型的消息都會通過同一個通道發(fā)送過來呻率,需要通過模式匹配機制做分發(fā)。它背后的邏輯是現(xiàn)實世界本來就是異步的呻引,不確定(non-deterministic)的礼仗,所以程序也要適應(yīng)面對不確定的機制編程。自從有了并行之后,原來的確定編程思維模式已經(jīng)受到了挑戰(zhàn)元践,而Actor直接在模式中蘊含了這點韭脊。
從這樣看來,CSP的模式比較適合Boss-Worker模式的任務(wù)分發(fā)機制单旁,它的侵入性沒那么強沪羔,可以在現(xiàn)有的系統(tǒng)中通過CSP解決某個具體的問題。它并不試圖解決通信的超時容錯問題象浑,這個還是需要發(fā)起方進行處理蔫饰。同時由于Channel是顯式的,雖然可以通過netchan(原來Go提供的netchan機制由于過于復雜愉豺,被廢棄篓吁,在討論新的netchan)實現(xiàn)遠程Channel,但很難做到對使用方透明蚪拦。
而Actor則是一種全新的抽象杖剪,使用Actor要面臨整個應(yīng)用架構(gòu)機制和思維方式的變更。它試圖要解決的問題要更廣一些驰贷,比如容錯盛嘿,比如分布式。但Actor的問題在于以當前的調(diào)度效率括袒,哪怕是用Goroutine這樣的機制次兆,也很難達到直接方法調(diào)用的效率。當前要像OO的『一切皆對象』一樣實現(xiàn)一個『一切皆Actor』的語言箱熬,效率上肯定有問題类垦。所以折中的方式是在OO的基礎(chǔ)上,將系統(tǒng)的某個層面的組件抽象為Actor城须。
評論區(qū)又見洗地,go的用戶就不要拼命洗地說用go的虛擬線程能夠作出actormodel了糕伐,這個明顯就是設(shè)計上的失誤砰琢,如果一開始就看準了有actormodel這回事的話,根本就不會設(shè)計成強制用戶使用虛擬線程良瞧,直接暴露os線程是最簡單也是最直觀的做法陪汽,就是因為go走出錯了路,挖了一個大坑褥蚯,然后為了actormodel而強行實現(xiàn)挚冤,只能說明這一開始就是一個錯誤,最后不計成本地去模擬出別人的模型赞庶,那actormodel從本質(zhì)上就有的性能優(yōu)勢還有什么意義训挡?之所以用actormodel就是沖著性能去的澳骤,而go這樣搞,只能作出比一般actormodel性能更差的實現(xiàn)澜薄,那還有什么意義为肮?你還是回去無腦堆虛擬線程算了,何必呢肤京?虛擬了一層又一層颊艳,其他語言的道路是通過保留線程api,然后增加虛擬線程api忘分,但是go是直接一上來就干掉了原始線程棋枕,強行封裝一層,然后再在這個基礎(chǔ)之上饭庞,去填坑戒悠,這么做的人一定是瘋了,這樣做還不如不做舟山,你看go用戶一直鼓吹的性能優(yōu)勢在這個時候,從模型上就開始表現(xiàn)出不足來卤恳,看看其他語言累盗,人家既有nativethread的包裝,又有coroutine/fiber突琳,我們要組合出eventlooop和actormodel來就很容易和直觀若债,然后搭配async/await就可以用非常簡單的方式寫代碼了,又回到最初的proactiveprogramming上去了拆融,但是go就少了nativethread這一步蠢琳,怎么模擬都會多出一層虛擬層,那這一層猶如隔靴擾癢镜豹,太不爽了
.
go是強行塞入n:m這一層傲须,而其他語言是保留了1:1,然后再在1:1的基礎(chǔ)之上增加了1:n趟脂,所以其他語言可以做到用async/await完全抹殺掉線程調(diào)度帶來的消耗泰讽,同時在這個基礎(chǔ)之上,也能做到無腦堆虛擬線程昔期,用車來比喻已卸,就是其他語言能做到手動擋和自動擋,但是go只能做到自動擋
九硼一、從寫程序的角度來看累澡,有什么不同
參考關(guān)于并發(fā)模型 Actor 和 CSP
1.假如,要做“關(guān)于取得RSS文章的單詞數(shù)”這樣的程序的話:
CSP:
步驟1:定義幾個channel般贼,保存不同處理的之間的數(shù)據(jù):
1愧哟,新文章ch
2惑申,文章內(nèi)容ch
3,單詞數(shù)ch
步驟2:然后寫相應(yīng)的處理程序:
1翅雏,新文章URL處理(把得取的新文章的URL圈驼,寫入“新文章ch”)
2,新文章內(nèi)容處理(把新文章內(nèi)容讀取下來望几,寫入“文章內(nèi)容ch”)
3绩脆,單詞數(shù)統(tǒng)計(對文章內(nèi)容進行單詞個數(shù)統(tǒng)計,寫入“單詞數(shù)ch”)
4橄抹,文章單詞數(shù)累加(讀取“單詞數(shù)ch”靴迫,把各個文章單詞數(shù)進行累加)
步驟3:在主程序中,定義上面的幾個channel楼誓,
再調(diào)用幾個處理程序玉锌,并把channel當成參數(shù)傳給處理程序
Actor:
步驟1:定義控制Actor,控制程序流程疟羹,功能如下:
1主守,當收到指令是“新文章URL處理”的話,調(diào)用“新文章URL處理”Actor榄融,并把處理結(jié)果返回給調(diào)用者参淫。
2,當收到指令是“新文章內(nèi)容處理”的話愧杯,調(diào)用“新文章內(nèi)容處理”Actor涎才,并把處理結(jié)果返回給調(diào)用者。
3力九,當收到指令是“單詞數(shù)統(tǒng)計”的話耍铜,調(diào)用“單詞數(shù)統(tǒng)計”Actor,并把處理結(jié)果返回給調(diào)用者跌前。
4棕兼,當收到指令是“文章單詞數(shù)累加”的話,調(diào)用“文章單詞數(shù)累加”Actor舒萎,并把處理結(jié)果返回給調(diào)用者程储。
(以上的每一個指令的執(zhí)行,都可以做成并行的)
步驟2:定義指令相對應(yīng)的處理程序臂寝。
步驟3:主程序章鲤,向“控制Actor”發(fā)指令,并把每次指令的結(jié)果當成參數(shù)咆贬,傳給下一次的指令調(diào)用败徊。
2.如果需求增加,在單詞數(shù)統(tǒng)計時掏缎,去掉”of/to/and”這些單詞的話皱蹦,CSP和Actor要怎么做呢煤杀?
CSP:
主程序:
1,定義一個新的ch
2沪哺,并增加對內(nèi)容過濾程序的調(diào)用沈自。
3,傳給“單詞數(shù)統(tǒng)計”程序的ch辜妓,要修改成新定義的ch
子程序:
1枯途,新加一個處理程序。
Actor:
主程序:
1籍滴,增加對內(nèi)容過濾Actor的調(diào)用
控制Actor:
1酪夷,增加一個新的內(nèi)容過濾指令
子程序:
1,定義一個新的內(nèi)容過濾Actor
十孽惰、protoactor-go簡介
Golang最好用的Actor框架 protoactor-go
go語言實踐-protoactor使用小結(jié)
Actor模型是一種適用于高并發(fā)的編程模型晚岭。早在1973 年 Carl Hewitt 發(fā)表的論文中定義了Actor,但一直流行于Erlang 語言中勋功。Erlang 被愛立信公司應(yīng)用于建立高并發(fā)坦报、可靠通信系統(tǒng),取得了巨大成功酝润,著名的rabbitMQ 就是Erlang的代表作燎竖。Java 語言的 Akka 庫里面角色的API 跟Scala 框架里面角色相似,后者一些語法模仿Erlang語言要销。
Golang中 始終缺乏一個代表性的Actor 框架,Golang自身的協(xié)程處理被廣大Golang推崇夏块,但從生產(chǎn)實踐來看疏咐,在大規(guī)模并發(fā)的情況下,協(xié)程并不是最佳的方案脐供,使用Actor框架編寫大規(guī)模并發(fā)服務(wù)通常被認為是更好的選擇浑塞。
今天給大家推薦是由瑞士的團隊Asynkron出品的Actor框架protoactor-go,從目前gitHub 上的Star數(shù)量觀察政己, protoactor-go 是唯一個star 過千的Actor框架酌壕,由于背后有專業(yè)團隊持續(xù)維護,因此穩(wěn)定性有保證,同時protoactor-go有.Net/Golang/Java/Kotlin 的跨語言平臺的實現(xiàn)歇由,給開發(fā)者提供了更多系統(tǒng)集成的方案卵牍, 特別是在互聯(lián)網(wǎng)公司,多語言共同開發(fā)環(huán)境下沦泌,優(yōu)勢明顯糊昙。
Actor框架的提出基本上與CSP差不多處于同一個年代,相對于后者谢谦,過去10年Actor還是要稍微火一些释牺,畢竟目前都強調(diào)快速開發(fā)萝衩,絕大部分的開發(fā)人員更關(guān)注業(yè)務(wù)實現(xiàn),這符合Actor側(cè)重于接收消息的對象(CSP更強調(diào)傳輸通道)一致没咙。ProtoActor的設(shè)計與實現(xiàn)猩谊,絕大部分和AKKA很相似,只是在序列化祭刚、服務(wù)發(fā)現(xiàn)與注冊牌捷、生命周期幾個地方略有差異≡#框架使用上整體與AKKA相同宜鸯,但受限于go語言,在使用細節(jié)上差別有點大遮怜。