Node.js、Scala、Clojure 等聲稱適合高並發的語言,分別具體適用於什麼情景,何種任務?


程序始終是需要由 CPU 執行的,所以真正並行的計算程序數量不會超過 CPU 的核心數。那麼,宣稱的「高並發」是什麼?

所謂「高並發」通常是在討論網路 I/O 的情況下出現的概念,通常而言,高並發意味著可以同時處理更多的網路請求數。提高網路請求並發量的方法是什麼?

  • 最基礎的是使用 epoll / kqueue 代替 select / poll,在註冊、獲取事件的部分從 O(n) 的複雜度提高到 O(1) [1] 。Node.js 應該是基於 &libev&libuv [2],Scala Akka 在 1.x 應該是基於 Netty [3],2.x 切換到 http://Akka.io 庫 [3][4],都是平台相關的高性能 I/O 事件處理框架。

  • 在底層獲得網路數據之後,如何進行處理是第二個步驟。
    • 最古典的做法是為每個 socket 啟動單獨的進程 / 線程並進行阻塞通信:好處是邏輯連貫,每個進程執行的就是整個協議對應的處理流程;壞處是在高並發環境下 OS 調度負載太高,浪費了 CPU 時間。
    • Node.js 的解決方法是所有可能阻塞的地方均使用回調函數指明之後的操作,所以 Node.js 可以儘可能快的切換到其它會話,並在可以繼續執行回調之後回到當前會話;壞處是:
      • Node.js 只有一個線程執行處理邏輯(可以通過 Node Cluster Module 擴展)
      • 回調函數構造的邏輯流程難以理解
    • Scala / Akka 的方法是構建非常輕量級的 Actor 進行處理邏輯的封裝。Scala 系統負責在有限的操作系統線程之上調度大量的 Actor 對象;因為 Actor 「輕」的特性,調度快速,不會造成 OS 的負擔;同時 Actor 又可以實現邏輯的連貫性,比回調方式寫成的代碼容易理解。

Clojure 本身沒有對網路 I/O 進行優化(更新:Clojure 1.5 出現了類似 Go 的 core.async [5]),與上面的話題根本聊不到一起去。它可以使用標準的 JVM 技術,並且最常見的 Clojure 應用部署還是基於 Jetty 或 Tomcat。Tomcat 默認的是 one-thread-per-client,後來新加了 nio connector 以提高 I/O 性能。另外有 http-kit 這樣的非阻塞 HTTP 框架可選。

Clojure 並不說適合高並發網路服務,但 Clojure 相比 Java 更適合併行程序的開發。它的特性在於

  1. 它本身是一門鼓勵使用 immutable 數據的函數式語言,因此相比基於對象狀態的 Java 程序,Clojure 的每個函數都更容易放到不同線程中執行
  2. Clojure 也提供了包括 pmap / future 等方法進行線程之間的調度和任務分派。&但因為 Clojure 的並發是緊緊綁定在 Java 線程之上的,比 Akka 的 Actor 要相對重量得多。& Clojure 提供了不同的調度方法:agent send 直接綁定 Java 線程因此相對重量級,而 pmap [6] / agent send-off 或 core.async [6] 等都使用線程池,通常為 #cores + 2 個線程。

  3. 相比 Node.js / Go, Clojure 的劣勢在於它默認沒有提供 async I/O 基礎,而使用 Java 的 I/O 的情況下非常容易被阻塞整個線程。需要同時正確使用 Clojure 提供的調度工具及合適的 async I/O 庫才能達到 Node.js / Scala (http://Akka.io) 庫整合提供的高吞吐能力。

相比 Go, Node.js 這些為高並發 I/O 而設計的語言,Clojure 提供了相對更為正交的功能集合(STM,並發支持,獨立的非同步 I/O 庫),雖然要達到同樣的目的可能略微複雜,但是靈活性也更高吧。

[1] Why is epoll faster than select?

[2] How is nodejs different from running lib event? - 根據評論中 @whtsky 指正,應為 libuv node/deps/uv at master · joyent/node · GitHub

[3] Akka 2.2 uses now spray.io instead Netty

[4] Introduction to Akka I/O

[5] Clojure core.async

[6] How many threads does Clojure"s pmap function spawn for URL-fetching operations?

Changelog:

2013-12-27 初版

2014-03-18 更新,添加 Clojure core.async 和 async I/O 部分內容,修改關於 Clojure 線程輕重的論述。

2014-08-19 更新,Node.js 依賴 libev 應為 libuv,感謝 @whtsky 評論指出。


  • node 是個單線程,所以你每個依賴於外部資源(文件系統,資料庫等)的調用都得能夠非同步,否則一個卡死就完了。

  • scala通常用Actor,比如akka來實現高並發,你可以想像actor是特殊的java class,互相只能通過message queue來溝通,不能直接方法調用。狀態保存在actor內部,每個actor只會被一個線程運行,所以不用擔心share resource的問題。

  • clojure作者覺得actor模式太麻煩,簡單的key value調用都要來回兩個message。clojure是LISP方言,函數式編程,區分identity和value,value不可變,給identity賦值新的value時都要經過語言內製定的function,由語言來保證一致性,讓編寫並發程序變得容易。

個人意見:

  • 如果你的業務邏輯不複雜,需要調用的外部資源在node里能非同步調用,推薦node,小快靈。

  • scala的play framework和akka actor有前後台web應用高並發的一站式解決方案,適合業務邏輯複雜,參與人數比較多的項目。

  • clojure學習曲線比較陡峭,更適合編寫應用內某個需要高並發的庫或者組件,強烈推薦學習。


我覺得吧,說這話的人估計只是看到了幾篇描述,然後就把東西混在一起了。我相信這些語言/環境對自己的描述下面都有並發(concurrent或concurrency)字樣,但上下文是不同的。

通俗地講,這裡並發要分兩種,一種叫做「吃IO」,一種叫做「吃CPU」,其中前者在目前的環境下變得越來越單指「網路IO」了。

Node.js顯然是吃IO的,它的賣點便是可以單,進程接受多少多少個連接,可以用很少的資源吃滿IO,這裡的資源的重點自然是指內存和CPU。

Scala和Clojure和Node.js完全是兩碼事,他們吃IO的能力完全是靠JVM決定的,JVM給個擅長吃IO的機制,比如nio還是aio什麼的,然後有人寫了方便吃IO的框架比如mina或netty什麼的,這才是決定Scala或Clojure這種JVM上的語言吃IO能力的關鍵。

這裡說Scala或Clojure這門「語言」時,真的是在說語言,所以說的是它提供的編程模型的能力,例如STM或者Actor等等。你說Java這門傻語言就做不到么?當然能做,不過又丑又煩導致沒人想去這麼用而已。這些編程模型的目的就是讓人可以輕鬆地樂意地去寫出高並發程序。這裡的高並發就更傾向於吃CPU了,我可以很簡單地寫出一個簡單程序,4核能跑,32核也能跑。當然它也能吃IO,只是沒什麼可多說的。

至於Node.js,首先它不是語言是JVM這樣的運行環境,其次它沒法用來做吃CPU的程序。你開N個進程不叫吃CPU,叫做各自完成獨立任務。用Akka那可是可以用來繪製出一副複雜的拓撲結構,計算在Actor之間丟丟丟,它自動算算算。

總之就是個把不同東西扯在一起的蛋蛋說法。


前面的評論對scala有些誤解:

1. scala通過future方式也完全可以實現回調式編程,而且對於非同步處理可以做到控制(比如可以把更多資源分配給某些回調)。我基於scala做過的一個項目,就是沒有一個actor。

2. scala可以做到純函數編程,包括STM都可以支持,當然也可以純對象編程,像java一樣。完全取決於如何使用。


erlang 默默流淚。


node.js 適合IO密集型任務,CPU計算密集型不適合,V8不善於操作大內存,本身單線程也是要注意的,會把event loop拖垮,只能用child process模塊來解

Clojure和Scala沒必要分那麼清楚,函數式編程先天適合併行,但是STM也不是萬能的,STM不是適用於IO,Akka也好,furture也罷,不管是Clojure的並發庫還是Scala的並發集合,都是為了簡化並發編程,而不是為了提高並發能力,底層就是JVM的厚重線程,Akka用的Forkjoinpool,線程池的調優以及JVM的優化才是提高吞吐量的關鍵

Erlang的process是基於綠色線程實現的(廉價)(SMP不是),這是與java線程(基於內核線程)的不同 其IO也是非同步的,和node不同,它不是交給操作系統,而是虛擬機層面實現的 但不表示Erlang就是最好的

補充

Erlang的搶佔式調度是為了實現軟時實,也就是低延時,這就導致大量上下文切換,一個大任務被切割成多個小任務多次執行,吞吐量直線下降

Go Haskell也同樣使用協程,但都以吞吐量為主,短任務可能會被長任務長時間執行而得不到立馬執行,也就是延時會很高,這就適合一些離線任務

相當來說node無Coroutine顯得有些簡陋,純CPS風格顯然是問題


Node.js的高並發,是基於沒有並發完全依賴非同步回調的方式來實現的。和其它方式相比,是徹底沒有Context-Switch的成本。代價就是你需要習慣回調式編程,這對習慣了同步編程的程序員來說剛開始是有障礙的。

但是Node.js帶來的好處非常明顯,它能達到並發性能僅僅跟你自己的實現邏輯有關,和底層架構的關係很小。這一點在做性能測試的時候非常明顯,高並發的成功率高,黑盒子少,定位性能問題容易。

Node Cluster Module以Fork工作進程的方式提供了利用多核利用能力,實際測試下來,如果IO不是大問題,也能獲得幾乎線性的性能提升。

極度簡單,所以可靠、高性能和易於維護。


高並發沒有Erlang?別個可是專業向並發的編程語言。

node 高效快速,相比而言啊

Scala,Clojure都是針對JVM,java項目可以考慮配合使用,Clojure適配的場景更多,Scala則是做一些API service會更好,充分利用函數當值用的特性

我是這麼理解的,語言無所不能,like money 哈哈,只有合適的沒有最好的,處理並發還是先對比下Erlang在言


node確實簡單一些,適合功能單一場景,對於複雜問題,處理回調會陷入坑中。node適合各個功能分配成服務模塊,然後用node或者其他語言進行調用,完成較複雜的應用。


照題主的意思是,無視我大Erlang?

Node.js 能不能算並發,我真不造,但我司的需求場景下,Node.js 完全滿足不了需求,而Erlang 綽綽有餘。

若干年前Scala大牛 Erlang大牛,出了個PPT

並發需求下的Scala及Erlang語言的比較與使用.ppt


嘗試過使用Node.js ,我的感覺是,以後一些簡單的Web應用可能會大量使用Node.js來實現,因為它安裝使用都太方便了,比之Python,Ruby等開發Web應用要配置一大堆複雜的東西來說簡單太多了。

但是,真心對動態語言+不停回調 這種形式無愛。

Scala ,說實在的,好複雜好複雜,一本一千頁的書,好像都不能窮盡它的語法。不過我覺得,某些領域是需要一門功能豐富的語言來實現,才能更簡單或者才能達成目標。

另外,我記得看過一篇文章,說的就是現在新出的語言總是聲稱自己高並發,但實際上,即使我們的CPU已經多核了,我們在編寫應用的過程中,用到高並發的情況並不是很多。

所以我還是覺得,最終真正做產品,還是應該選擇C, C#,PHP, Java這樣更大眾化的語言,程序員好找,工具鏈豐富,各種實戰經驗也多


Scala是半函數式語言,Clojure是函數式語言,它們與Node.js所謂的「並發性」完全不同。

函數式語言由於「不可變性」的影響,數據的Contention問題得到緩解,加上軟事務(soft-transactional)支持,所以一般都聲稱自己是高並發(例如Erlang)。

Node.js的並發實際上僅僅是回調式處理(某種形式的非同步)。


推薦閱讀:

請問各位大神,spark的ML和MLLib兩個包區別和聯繫?!?
為什麼有些程序員看不起 PHP 這門語言?
scala case class 這時候該怎麼用?
代數數據類型是什麼?
kotlin和scala兩種語言的對比?

TAG:編程語言 | Scala | Nodejs | Clojure |