標籤:

Golang 的並發與 Erlang、Scala、Node.js 和 Python 的並發模型相比有何特點?

最近學python,發現也有多線程的概念,其實golang很大的特點就是並發性,但與其他也支持並發的語言(erlang,scale,node.js,python)相比有何特點,哪種實現比較優秀?


erlang和golang都是採用了CSP的模式,Communicating Sequential Processes。但是erlang是基於進程的消息通信,go是基於goroutine和channel的通信。至於python就是一個多線程而已,沒有什麼其他的技術,nodejs是一個非同步操作庫,思路完全是不一樣的。

我也不知道那種比較優秀,我覺得就看你喜歡那種實現方式而已。


找到一個很不錯的博客,對 Erlang 和 Go 在若干方面做了相當詳細的分析。我不是作者,為避免騙贊嫌疑匿名發上來共享一下,也希望博主能繼續分享更多好東西:

AmaL"sbLoG

附部分博文標題:

Erlang Go 的並發調度淺析

Erlang Go 消息傳遞機制初探

Erlang Go 的IO優化策略簡介

關於Go語言調度器實現細節的補充分析

Go 語言對OS系統調用的處理


來個我了解的優劣對比,沒有哪個東西是絕對好的,nodejs沒有看起來那麼差,golang也沒有看起來那麼好,根據自己的需求選取吧。

nodejs:

內部是多線程,但是用戶的代碼全都執行在單個線程內,利用操作系統的non-blocking I/O來實現並發。

好處:

1. 單核心上,基本上這是速度最快的方案。

2. runtime的核心實現起來比較簡單,簡單意味著容易維護,bug容易修正。

壞處:

1. 程序員的負擔加大。很多人都難以適應層層遞進的callback,好在還有一些庫可以緩解。

2. 某個任務如果執行CPU密集型計算,會阻塞其它任務。

3. 單進程內無法利用多核。

python :

沒深入研究過,不過由於GIL的存在,猜測跟nodejs應該是差不多的

golang:

最核心的壓根就不是actor模式/CSP神馬的,而是調度器。放在C++和其它任何高級語言里實現actor/CSP也是個非常簡單的事,但要寫個調度器則非常困難,golang/erlang幫你做了這一步。

golang採用N:M方式進行任務調度。簡單說就是程序啟動時設定啟用幾個OS native線程(GOMAXPROCS),每個線程運行一個scheduler(由golang的runtime提供),程序運行時每個scheduler維護自己的task列表(goroutine),並進行調度。

調度方式跟nodejs類似,遇到I/O時,把時間片讓出來給其它任務使用。

好處:

1. 多任務的實現方式用了coroutine (cooperative tasking),用戶的代碼在邏輯上順序放在一起,而無需分散到各個callback里。當coroutine/goroutine中執行I/O時,用戶代碼在「邏輯上」發生了block,但物理上已經yield control到scheduler代碼中,scheduler會指派新的任務在當前的OS線程上執行。

2. user space context switching,速度很快,佔用的資源也很小。

3. 單進程內很容易利用多核。

壞處,或潛在的問題:

1. golang幫用戶做了不少事,所以不了解基本原理的用戶很容易因誤解而導致意想不到的後果。由於scheduler運行在user space,無法搶佔式的(preemptive)調度任務,所以每個任務必須顯式的yield才能出讓時間片給其它任務,這個yield在golang標準庫中都幫你做了,但如果你調用到C代碼中然後進行阻塞式I/O(如果調用了第三方庫,這個問題可能很難察覺),這個scheduler會因為無法得到時間片而堵死其上的所有goroutine。所以跟nodejs一樣,執行CPU密集型的計算也會導致本scheduler上的所有goroutine發生阻塞

比nodejs好一點的是其它scheduler上的任務任然可以得以調度。

要緩解這個問題,請在每計算出一些數據後調用runtime.Gosched()進行手工yield(這是典型的cooperative tasking,可以簡單的理解為golang的標準庫在每次執行I/O的時候,都自動幫你調用了這個方法,但在你自己的代碼里,只能靠自己)。

2. N:M的schduler實現起來太複雜,需要考慮的各種邊邊角角的情況太多。FreeBSD和NetBSD曾經都支持N:M的green thread調度,但後來都拋棄了這個方案改為更簡單的1:1(就是我們現在普遍使用的OS native thread模型)。也就是說golang/erlang拾起了這個被操作系統拋棄的方案,實現到了語言里。想要穩定下來可能需要很長時間,可能還需要面對一些新的複雜性,比如現在伺服器系統上的NUMA架構,要在其上實現一個高效的、無bug的work stealing scheduler(把另一個scheduler的任務拿過來放到自己這裡運行,以防止資源浪費),那是相當難的,即便實現出來了,很可能在其它方面(比如性能)要做出很大的取捨。

由於N:M方案存在很多取捨的問題,在某些場景下並不好使,rust在最近的幾個版本中已經把N:M改為可選了,預設是1:1方案。

未來最好的並發方案還是讓操作系統把native thread變的越來越輕量,從而徹底消除掉user-level的N:M scheduler,據說google已經對linux做了這麼一個patch出來自己在用,且準備提交到upstream上去,但具體情況和時間未知。

總的來說nodejs的方案是核心端輕,用戶端重,golang/erlang正好反過來,把複雜性從用戶身上轉移到了語言實現里,這兩種哲學目前還不好說誰更好誰更壞,golang/erlang總體看起來更好,但也有自己的缺點,這是要注意的。


Node.js為了高並發的非同步編程模型是反人類的~~~


Python和Ruby(Ruby1.9以後,1.8之前是green thread)的多線程都是native 線程, 但是它們都有GIL, 但並不意味著他們的多線程就是假的,多線程的出現,本來就是一種處理並行的方案,是為了並發,GIL只是為了保護多線程編程引發的問題,雖然不是絕對保護,但也是一種機制,但並不影響我們使用多線程。

至於Erlang和Scale 他的並發模型是Actor模型, Golang是CSP(communicating sequential process)模型,他倆都是差不多的數學模型,詳細的樓主可以查找具體的資料,詳細的我也不太了解。

Node.js 處理並發是Reactor模式,是一種事件驅動型非阻塞IO框架, Ruby的Goliath框架也是使用這種Reactor模式, 也能處理高並發, 具體樓主可以可以研究下eventmachine。

現在比較流行的是事件驅動的Reactor模式,比如Node.js, Ruby框架Goliath來處理高並發。

至於哪種好, 這個沒有可比性,都是處理並發的一種模型,解決問題就行了。

但是不同的語言編寫的並發代碼不同, 比如node.js里你需要寫很多callback函數,代碼不易維護,但是Ruby里使用Fiber(其實就是協程)來避免寫過多的callback,這樣就可以寫出易讀性可維護的代碼。 樓主可以從這個方面去考察。


開始覺得nodejs很驚艷,用了go之後,再也不想用callback。。。。


只熟悉erlang和golang

golang是基於CSP的(Rob Pike之前也搞了好幾個並發語言都是基於CSP)。

大家都說erlang是基於actor模型的,但是Armstrong在郵件裡面明確否認過。

Armstrong寫過一個erlang歷史的文章 http://webcem01.cem.itesm.mx:8005/erlang/cd/downloads/hopl_erlang.pdf

不過粗看的話,在模型上確實沒什麼差別。

我覺得golang和erlang的主要區別是在通信方式上。

erlang是完全的非同步通信;golang雖然也可以設置channel的buf,但是默認是0,也就是同步通信。

erlang面對多核甚至是分散式環境都沒什麼問題。但是golang在多核調度上好像還有問題,不能真正把任務調度到多個核上(不知道是不是我看的資料比較老?)

總的來說,erlang更偏向非同步通信的場景;golang更適合同步比較多的場景。

另外一個關鍵的點是內存模型

erlang是一個嚴格的函數式編程語言,沒有副作用。

golang新提出了一個happen-before的概念。golang仍然支持全局變數,但是對全局變數的操作不保證並發安全,只有通過channel才能保證時序。

詳細情況見下面的鏈接。

go_mem -
golang-china -

內存模型


老實講erlang是所謂並發目前我見到的最佳解決方案,沒有之一。

go和rust大體上是走了抄erlang的路子,不過引入了coroutine和channel的概念,簡單的來說就是一個我等待IO的時候控制權交給你這樣的行為,channel負責調度和通訊。然後,python也有類似的解釋器,叫stackless python,stackless,一下子就解釋清楚了,具體的可以搜個維基,幾句話講不清。

而傳統意義上python/ruby的並發是線程,以前ruby不是native的,現在也是native的了,GIL負責大顆粒的線程安全,但是,目前流行的做法卻還是coroutine這類的實現,ruby我不知道是啥,python的有gevent,eventlet啥的,底層是greenlet,而這個greenlet你可以理解為就和go的goroutine。

但由於解釋器先天限制,Cpython能做到的也就是差不多而已,因為缺乏channel,因此有一個調度中心負責干這事,等於就是說我等待IO的時候控制權給調度中心調度中心再去給你控制權這樣。

至於node.js,event-driven而已,本質上是我剛才說的那種行為的抽象,所以會有callback hell,當然你封裝一下就好了,大體行為上其實沒多大區別。

總之呢,高並發通訊erlang/stackless python,一般高並發呢go/rush,提高生產力呢python的gevent這類就好了,看場景。


與其他使用native thread並發方案的語言比(C, Java, Ruby, Python),可以更容易的復用io阻塞時的cpu時間,而不用刻意使用非同步io破壞順序執行的流程模式。

與直接嵌入非同步io的語言比(NodeJS),可以進程內調度多核CPU,達到更好的並發。而且依舊保持了順序執行的流程模式,便於開發。

與Erlang比,更多是過程式語言和函數式語言,靜態編譯和動態解釋的區別,並發模型在數學上有細微的不同,但在工程角度,解決的問題都是一樣的。執行效率上Go要好很多,但是分散式庫目前還是Erlang佔優。如果開發業務流程,過程式的Go要更適合函數式的Erlang。如果問到當前時間,想到的是個變化的值,而不是一個序列的話,就還是用Go吧。

與支持yield的語言比(C#, Python, Lua),要具體分析了。有的語言實現是不支持多核,僅僅是非同步io的語法糖,那Go的優勢在於利用多核。對於能利用多核的語言,自動調度應該是個好處,只是並沒有那麼顯眼了。

不過,作為一個新語言,Go沒有歷史包袱,並發功能不會因為某個庫的歷史原因而打折扣,而且執行效率很不錯,上手快,才是工程上更大的優勢。


Erlang的設計初衷就是「面向並發編程」。

如果題主沒有接觸過函數式語言,或者對OO語言比較熟悉,並且想要使用Erlang的威力,那麼非常推薦Elixir。

Elixir可以任意調用Erlang中的庫,包括久經考驗的OTP工具。語法非常簡單,能看到Ruby的影子。


goroutine類似於lua的coroutine,有自己的堆棧,可以認為是用戶層的線程。

最大的特點就是golang的標準庫都基於goroutine來進行設計,main函數本身就運行在一個goroutine裡面。在一個goroutine裡面,你所寫的的代碼是線性執行的,不需要考慮什麼非同步或者回調,包括調用標準庫的任何API,這個是goroutine的獨家特點,我暫時還看不到有其他語言能做到這點。

這個跟Actor是不同的,在我看來,Actor只是一種回調形式而言,它迴避了如何處理IO(阻塞或非阻塞),也不像erlang的process那樣可以被搶佔調度。

goroutine裡面運行的代碼可以劃分為:

1. 可以被底層事件框架管理的IO操作

在linux上,就是能註冊到epoll的事件,例如最經典的socket讀寫,goroutine在遇到需要阻塞的時候會自動睡眠,等到可以繼續進行操作的時候自動被喚醒

2. 阻塞IO(例如磁碟上的文件讀寫)或者C調用

面對這種情況,goroutine會退化為系統線程,並且由操作獨佔該線程直到做完為止

3. CPU密集型的代碼

類似erlang的process,goroutine在某些切入點進行搶佔調度,例如函數調用,這個有利於使得時間片均勻分布在不同goroutine上


最近並發的概念火的跟大數據似的,說真的,中低級程序員,基本上就沒有多少場景要寫並發程序。等你變成高級工程師,要寫很多並發程序的時候,你就根本不需要來問這個問題了。

還是回答一下你的問題吧:這裡面最優秀的就是Erlang了,Scala和Go都在一定程度上模仿Erlang。

Node.js的話,如果你確定整個團隊都能夠在層層回調之中搞明白情況,那就用唄


推薦閱讀:

現在想再學習一門編程語言,應該選擇go還是python?
為什麼用golang作為遊戲服務端的開發語言,它的並發性如何?golang有什麼優點?
golang的goroutine是如何實現的?
有哪些公司在用 Google 的 Go 語言?成熟度和 Erlang 比起來如何?
go語言以後會不會成為主流web開發語言?

TAG:Go語言 | 並發 |