[ ECUG 專題回顧]《再談 CERL:詳論 GO 與 ERLANG 的併發編程模型差別》-許式偉

許式偉:咱們開始,先介紹一下ECUG,從07年開始,最先在珠三角珠海廣州深圳,在珠三角興起,最先是Erlang的社區。大概到10年的時候改名爲實時效雲計算的羣組,最先的時候也不侷限於Erlang,而是會有各類語言如Haskell、Scala等..,其實根本就沒有限制,只要是中途穿插後端開發運維的實踐均可以,後來咱們就正式更名爲實效雲計算的羣組。,範圍擴也蠻大到全國,基本上北京、長三角都有舉辦過。因此應該說到今天堅持了也差很少有8年,總共有9屆,07年的時候辦了2屆。這個是ECUG 的歷史。南京是第一次辦這個大會,我大學是在南京唸的。今年想的挺久的,咱們但願是可以把這個火花在全部的城市都可以點燃,因此今年就選擇了南京這樣一個對我來講比較有特殊意義的地方。 程序員

我開始個人話題了。其實,這個話題我其實在杭州的ECUG上時候講過,可是當時講的比較婉約,實際上我當時已經意識到Erlang的編程風格的問題,可是解刨得並不完全,,因此我今天又回頭談一下這個話題,用相對比較詳細的方法去對比說Go與Erlang併發模型二者到底有什麼不一樣?由於基本上我知道ECUG 發展歷史的人都有困惑,爲何我會從Erlang轉到切到GoO。 算法

這個是話題的來由仍是想從頭談談GO和Erlang的併發,我在09年開始決定放棄用Erlang本身在C++裏面從新造一個Erlang的編程模型,CERL這個網絡庫最初的出發點是這樣的,因此CERL的C表明的是C/C++、ERL表明的是Erlang。最先的思路是簡單把Erlang搬到C++,由於Erlang的程序員確實比較難招,可是後來發現其實Erlang的併發模型並無我想象得那麼舒暢,就搞了一個CERL2.0,它是對Erlang模型的反思和改進。最後發現這個反思最終獲得的結果和Go的併發模型徹底一致。因此CERL1.0和2.0的對比,其實你能夠認爲是 Erlang 和 Go 的對比。其實不少人問我這個話題,爲何CERL沒有開源?緣由是我以爲過了那個時間點了,開源沒有太大的意義,因此我以爲不太想誤人子弟,由於我本身最先是C++的粉絲,可是我接觸Go之後有一個很是強烈的願望我但願C++這樣的東西最好仍是可以早點退出歷史舞臺,關於這個話題我曾經有一個演講,是講Go與七牛的歷史,我反思了個人C++奮鬥史,那個演講我會後給你們做爲一個補充的材料放上去。 編程

既然沒有開源那應該怎麼理解CERL呢?其實這個世界有相似的東西,這個Pth是我最近才知道的,中途還看到過另外一個開源庫,大概在08年開源的,惋惜我一時找不到項目網址了。當時我看了一下跟CERL差很少,可是沒有CERL庫寫的完整,可是Pth這個庫出現歷史是很是早的,並且是GNU下的一個開源項目,它是99年就已經起動了,到06年左右就再也不更新了。它的出現時間很是早,併發模型和CERL是幾乎同樣,並且完成度很是高,畢竟發展了7年,因此要理解CERL其實也是研究一下這個庫基本上就差很少了。可是我其實有一個反思,爲何這個Pth這麼好的東西爲何沒有流行起來?第一個是生不逢時,出現的太早因此沒有引發注意,由於其實大概談多核時代這樣的概念在個人印象當中是07年左右開始有這樣的概念,Erlang也是那個時候才逐步被人意識到價值的。第二個就是否是標準庫,由於這樣一個庫侵入性是很是強的不光你意識它好還有別人也意識到,不然有一個問題別人寫庫你是不能用的,這個就是侵入性和傳染性,因此會致使其實沒有辦法真的把這個庫用起來,這種有侵入性和傳入性的庫最初興起的時候須要有一些激發的條件,它沒有這樣的條件。這和 C/C++ 中 GC 比較難流行是相似的道理,由於 GC 也有侵入性和傳染性。 第三個是從實現講,仍是有瑕疵的,最大的瑕疵就是輕量級進程並非真的輕量。輕量級進程的核心不僅是要性能好, 更重要的是資源佔用要小,但多數狀況下的這種資源佔用小這個實際上是比較難實現的, Go 在這一點作的比較好,有棧的自動增加,最小的棧最初的時候能夠只有 4K , 這樣每一個輕量級進程從資源佔用來講真的很輕量。 可是要達到這一點這個絕大多數的庫都很難作到。像CERL咱們只能作到說你本身指定說這個輕量級進程棧要多大,可是對程序員來講指定棧大小是很是困難的事情,有很大的心智負擔。要理解CERL,研究這個Pth是比較好的學習材料,固然第二個我認爲就是直接學習Go的Runtime了。從輕量級進程來說,它的底層跟CERL是同樣的。 後端

輕量級進程模型我很是早就提了,從我最初提倡Erlang的時候就已經提出了這個概念,什麼是輕量級進程模型呢?很簡單就是兩個,一個是鼓勵用同步IO寫程序邏輯,第二點是用盡量多的併發進程來提高IO併發能力。這和異步IO併發模型不同的,哪怕你是單線程也能夠作高併發。 服務器

全部輕量級進程併發模型的核心思想都是同樣的,第一讓每一個輕量級進程的資源佔用更小,這樣就能夠建立百萬千萬級別的併發,可以建立的進程個數惟一限制就是你的內存。每一個進程資源佔用的越小可以產生的併發能力就越高。作服務端你們都知道,內存資源是很是寶貴的資源,但某種意義來講也是很是廉價的。第二就是更輕的切換成本,這是爲何把進程作到用戶態,這個和函數的調用基本是在同一個數量級的,切換成本很是很是低。可是若是是操做系統進程則至少要從用戶態到核心態再到用戶態的切換。 網絡

講一下輕量級進程模型的實現原理,這個是蠻多人仍是比較關注的。我以前比較少談這個,可是我今天咱們詳細的談一談輕量級進程究竟是怎麼回事。先談談進程,所謂的進程究竟是什麼樣的東西?其實進程本質上無非就是一個棧加上寄存器器的狀態。進程的切換怎麼作呢?就是保存當前進程的寄存器,而後把寄存器修改成另一個新進程的寄存器狀態,這樣至關於同時也切換了棧,由於棧的位置其實也寄存器維持的(ESP/EBP)。這個就是進程的概念,哪怕操做系統的內核幫你作的本質上也是這樣。因此這些事情是在用戶態同樣能夠作到,而不是不能作到。本質上來說和函數的那個調用你能夠認爲也是差很少,由於函數的調用也是保存寄存器,只是相對少一些,至少不會切換棧。因此本質上講實際上是這個切換的成本是和函數調用是基本上差很少的,我本身測過,大概就是函數調用的10倍左右,基本仍是在一樣的數量級範疇。那麼在這樣一個輕量級進程的概念引入之後,實際上整個輕量級進程的程序物理上是怎麼樣的?底層其實仍是線程池加異步IO,你能夠把這個線程池中的每一個線程想象成虛擬CPU(VCPU)。邏輯的輕量級進程(routine)的個數一般是遠大於物理的線程數的,每一個物理的線程同一個時刻確定只有一個routine在跑,更多的routine是在等待當中的。可是這個等待中的routine有兩種,一種是等IO的,就是說我把CPU交給他也幹不了活,還有一種是IO操做已經完成,或者是本身自己並無等任何前置條件,總之是能夠參與調度的。若是某一個物理的線程(VCPU)它的routine主動的或者是由於IO觸發了一個調度,把線程(VCPU)讓出來,這個時候就可讓一個新routine跑在上面,也就是從等待當中而且能夠知足調度的routine參與調度,按照某種優先級算法選擇一個routine。因此輕量級進程調度原理是這樣的,它是用戶態的線程,而後有一個非強佔式的調度機制,調度時機主要由IO操做觸發。發生IO操做的時候,IO操做的函數是這樣實現的:首先發起一個異步的IO請求,發起後把這個routine狀態設置爲等待IO完成,而後再出讓CPU,這個時候也就觸發調度器的調度,這個時候調度器就會看看有沒有人等着調度,有它就能夠切換過去。而後再IO事件完成的時候,IO完成後一般會有一個回調函數做爲IO完成的事件通知,這個會被調度器接管,具體作什麼呢?很簡單就是把這個IO操做所屬的routine設爲Ready,能夠參與調度了。由於剛剛它的狀態是在等IO,就算調度到它也沒有辦法作事情。而 Ready 的話就是讓這個routine能夠參與調度。還有一種狀況就是routine主動出讓CPU,這種狀況下routine的狀態在切換的時候仍然是Ready的,任何的時間均可以切到它。以上幾個基本上是非強佔式的調度裏面最基礎的幾個調度器觸發的條件:IO操做、IO完成事件、主動出讓CPU。可是其實在用戶態的線程也能夠實現強佔式的調度,作法也是很是簡單的,調度器起來一個定時器,這個定時器定時出發一個定時任務,這個定時任務檢查每一個正在執行當中的routine的狀態,發現佔CPU時間比較長就可讓它主動地讓出CPU,這就就能夠實現強佔式的調度。因此哪怕在用戶態,它能夠徹底實現操做系統進程調度全部作的事情。這就是輕量級進程的實現原理。 併發

下面一個問題是Erlang和Go到底有什麼不一樣?這兩個不都是輕量級進程的併發模型?應該說它們的基礎哲學確實差很少,可是細節上有很是大的差別,而不是一點點的差別。主要的差別是在於幾點:第一個對鎖的態度不同,第二個對異步IO的態度不同,第三個不算最主要的細節,可是是次重要的細節,二者的消息機制不太同樣。 運維

首先談談對鎖的態度,Erlang 對鎖很是反感,它認爲變量不可變能夠很大程度避免鎖,Erlang 認爲鎖有很大的心智負擔因此不該該存在鎖。 Go 的觀念是鎖確實有很大的心智負擔,可是鎖基本上避無可避。咱們先宏觀看看鎖爲何是避無可避的,首先服務器首先是一個共享資源,是不少用戶在用的,不是爲某一我的用的, 因此服務器自己就是共享資源, 一旦有併發就是這些併發請求就在搶這個共享資源。咱們清楚, 一旦有人共享狀態而且相互強佔去改變它的話,這個時候必然是有鎖的,這點是不以技術的實現細節爲轉移的, 固然這個分析是從宏觀角度講,後面我還會講技術細節,來談鎖爲何不能夠避免。 異步

Erlang爲何沒有鎖呢?實際上Erlang的服務器是單進程(Process)的,是邏輯上就無併發的東西。一個Process就是一個執行體,因此Erlang的服務器和Go的服務器不同,Go的服務器必然是多進程(goroutine)一塊兒構成一個服務器的,每一個請求一個獨立的進程(goroutine)。可是Erlang不同,一個Erlang服務器是一個單進程的東西,既然是一個單進程的首先全部的併發請求都進入了進程郵箱(後面會談這個進程郵箱),而後這個服務器從進程郵箱裏面取郵件(請求的內容)而後處理,因此Erlang的單個服務器並無併發的請求,這個是他不須要鎖的根本緣由,其實並非由於它沒有變量,變量不可變這些。由於你們都知道單線程的服務器必定是沒有鎖的。那麼可能會有人問,那Erlang怎麼作高併發呢?實際上是兩點:第一是每一個Erlang物理的進程會有不少的服務器,每一個服務器相互是無干擾的,它們能夠併發。第二是單服務器想要高併發怎麼辦?Erlang對這個問題的回答就是請異步IO。 函數

可是異步 IO 給 Erlang 帶來了什麼麻煩呢?首先是服務器狀態變複雜了,這個複雜是很是很是要命的,這致使我最後認爲 Erlang一旦引入了異步 IO 以後,其實比正統的異步 IO 編程模型還要糟糕。咱們看幾點。首先爲何會有中間狀態的引入?由於有異步 IO,因此剛剛的某一個請求其實尚未完成,可是它必須把時間讓給另一個請求,因此這個時候服務器就要維持剛剛沒有完成的那個請求的中間狀態。一旦有中間狀態的話,這個服務器的狀態自己就不乾淨,單次請求的中間狀態要服務器來維持狀態,這個是很是不合理的事情。第二,這個服務器的中間狀態將致使比較複雜的狀態機,這裏面的狀態很複雜,由於服務器不僅是要維持一個請求的狀態,而是全部的未完成的請求的狀態都要它來維持。第三,這些中間狀態會致使有鎖的訴求,爲何會有鎖的訴求我下面會講。因此Erlang雖然試圖避開鎖,可是一旦有異步 IO 其實本質上仍然沒有辦法避開鎖。

爲何Erlang沒有避開鎖呢?剛剛咱們已經講了,本質上講是由於有進程郵箱的存在,並且Erlang的服務器是單進程(執行體),因此常規上沒有併發因此不須要鎖,可是一旦引入了異步IO之後就會有僞的併發。既然是單的進程,不可能真的有併發,但若是咱們把Erlang的進程(Process)也是認爲一個VCPU,由於有請求沒有完成,因此同時就有不少併發請求在同一個VCPU上跑。這中間可能出現某個請求須要暫時佔用某種資源是不能釋放的,會出現一些相互互斥的行爲。一旦有這樣的行爲就必然有鎖,這個鎖雖然不是操做系統實現而是本身實現,具體可能會體現爲相似BusyFlag這樣的東西,這其實就是鎖。全部鎖的特徵,好比說忘記把這個釋放了,整個服務器就被掛住了,它的行爲和全部的鎖的行爲是徹底同樣的。有人會說我根本沒有操做系統鎖,的確單線程的程序必然不會有操做系統的鎖,可是不能懷疑其實咱們代碼裏面是有鎖的。

因此,在對鎖的態度這個問題上,Erlang竭力避免鎖,可是實質上只是把鎖的問題拋給用戶。而Go則選擇接受了鎖沒法迴避的事實。

咱們再看對異步IO的態度。Go認爲,不管如何都不該該有異步IO的代碼。而Erlang從輕量級進程併發模型來講不是很純粹,它沒有排斥異步IO,是一個混雜體,是異步IO編程加上輕量級進程模型的混雜,這個混雜的結果是讓Erlang的編程,一旦用了異步IO的話,實際上是比單純的異步IO編程的心智負擔還要大。

最後一個細節是我剛剛講過的次重要的概念,它是 Erlang的進程郵箱,全部發給Erlang進程的消息都會發到這個進程郵箱,Erlang提供郵箱收發消息的元語。Go則提供了channel這樣的通信設施,這個channel能夠輕易建立不少個,而後用它進行進程通信。相比之下,Go的消息機制抽象更輕盈。消息隊列和進程是徹底獨立的設施。

那麼,咱們再看看咱們應該如何去理解Go的併發模型?Go的併發模型很新嗎?其實不是的。我在不少的場合都講過,Go的併發模型其實根本不是一個創新性的東西,爲何呢?由於Go的併發模型是從有網絡以來咱們就是這麼寫程序的,從第一天寫網絡程序的時候咱們寫的就是Go推崇的併發模型。那麼問題在哪裏呢?爲何你們最後放棄了最古老的併發模型?緣由是由於OS的進程和線程過重,致使了你們人們去千方百計提升IO併發的時候用了一些歪招,也就是今天你們普遍接受的異步IO編程範式。這個異步IO變成範式帶來的問題是程序員的編程心智負擔大大加劇。因此Go的創舉有兩點:第一點就是價值迴歸,其實最古老的併發編程模型就是最好的併發模型。它的問題是執行體的成本,因此Go的最重要的事情就是讓執行體的成本無限下降,你們知道Go的最新版本棧最小能夠到4K,小到讓不少人以爲難以想象。因此這一點Go實際上是從實現層面解決的,而不是從編程範式解決的。Go第二個創舉是讓執行體變成了語言內建的標準設施,剛剛我說那個Pth庫流行不起來是由於這種併發模型是有傳染性和互斥性的,這個系統當中不該該有兩個這樣的設施,而若是你們用的設施不同,它是會排斥的,這個傳染性必需要求執行體必須成爲標準化的東西。並且這已是什麼年代了?多核時代已經喊了快十年了,可是咱們你們能夠看到,幾乎沒有多少語言把執行體這個做爲語言內建標準來作,我以爲這是Go很大的創舉。

讓咱們回顧一下,Go的併發模型其實就是這一頁提到的東西。它是最古老的併發模型。現代的操做系統,以及你們學的操做系統原理,和Go裏面的概念徹底一致。首先這個併發模型涉及的是執行體這樣一個概念,也就是Go的goroutine,而後一次是原子操做、互斥體、同步、消息,最後就是同步IO。這些就是Go的併發模型全部包含的內容。

那麼最後一個問題,Erlang中是否是能夠實施Go的併發模型?在Go裏面實施Erlang的併發模型是比較容易的,可是反過來想Erlang裏面可不能夠實現Go的併發模型呢?原則上是不能。由於在Erlang當中進程不能實現共享狀態,這個是他反對鎖的最重要的基點。進程不能共享狀態,因此不用鎖,但其實我認爲這個是最大的問題,爲何呢?由於Erlang收到請求之後沒有辦法建立一個子的執行體,而後讓它處理某一個具體的請求不用再管它。可是Erlang裏面進程沒有共享狀態,你要改服務器狀態必須用異步IO的方式,把事情作了再把消息扔給服務器對他說你本身改狀態。經過消息改服務器狀態,這個成本是比較大的,並且帶來了不少問題。因此我認爲Erlang的用消息改這個狀態是很差的作法,繞了一大圈沒有本質改變任何的東西。固然,若是我在Erlang裏面非要作到Go的併發模型也能夠,這須要對Erlang作一個閹割,若是咱們讓Erlang的服務器都無狀態的話,是能夠實施Go的併發模型。什麼樣的服務器是無狀態的?你們可能很容易想到PHP服務器。它把狀態交給全部的外部的存儲服務,由存儲服務來維持狀態。若是說Erlang的服務器是無狀態的是能夠實施Go的併發模型,由於全部的狀態都經過修改外部的存儲。可是這樣的話Erlang程序員確定是很傷心,看起來Erlang語言並無帶來什麼實質性的好處。因此個人結論是:是時候放棄Erlang了。

這就是個人演講內容,謝謝你們!

相關文章
相關標籤/搜索