Event Loop
Event Loop(EL)是一個比較重要的概念,關系到是否了解 Node,是否可以寫好 Node 代碼。同時 EL 難以了解并産生了很多困惑或誤解,網上流傳着很多對 EL 了解的版本,這些文章大多數從技術的角度去分析。直接從技術實作去探讨 EL,可能容易陷入具體的技術細節,而忽略 EL 本身是什麼。先從概念上解釋 EL,再到具體如何實作 EL,這樣可能會更通俗易懂。
從名字來看,Event Loop 由兩個單詞組成(複合名詞),Event 和 Loop:
- Event:事件,比如定時器到時、收到新的請求、檔案可讀
- Event Source:事件源,産生事件的對象,比如定時器
- Event Queue:事件隊列,記錄産生的事件
- Loop:循環,反複地連續做某事(處理事件)
EL 字如其意,經過咬文嚼字,可以猜出 EL 的大概意思。EL 不斷等待事件源産生事件,然後處理産生的事件。比如服務端收到新的請求(事件)事件就可以等待接收用戶端的資料(處理)。
同一時間可能産生很多同類型事件,需要儲存在 EQ 中按順序處理。如果事件類型複雜多樣,還需要把不同類型的事件分别儲存在不同的 EQ 中,并根據事件類型的優先級采取不同的處理。比如在 Node EL 中 Next tick 隊列的事件比其他 micro tasks 隊列的事件優先級高。
Abstract
Event Loop 是一個抽象的概念,不是具體的。 對于計算機來說,EL 是一個程式結構/模型,Nginx EL、Redis EL 和 Node.js EL 是 EL 的實作,是具體的。 每種語言可以有自己的 EL 實作,甚至同一種語言中 EL 會有不同的實作,但是大多數的 EL 都是大同小異的。在計算機中 EL 往往使用一個線程實作。
JS EL Visualization
Philip Roberts 曾在 JSConf 2014 分享了《What the heck is the event loop anyway》,介紹了他對 EL 的了解,以及 EL 的可視化。雖然這個可視化比較簡單,但是可以幫助更好的了解 EL。
Dinner
做飯是一件很平常的事,同時也需要花費時間和心思。一般做飯有這幾件事:煮米飯、煲湯、炒菜。
假如分别用這三種方式去做飯:一個人一件事情一件事情按順序做;多個人每個人做一件事件;一個人同時做多件事情。
One by One
一個人按部就班,做好一件事再做下一件事。煮米飯 -> 煲湯 -> 炒菜。
雖然可以專注做好一件事,但是耗費大量時間。
Collaboration
多個人同時一起做,每個人做一件事。A 煮米飯,B 煲湯,C 炒菜。
相比之前一個人做飯效率提高了,但是浪費了人力資源。(但是一起做飯有樂趣)
Busy
一個人同時一起做多件事,一邊煮米飯一邊煲湯一邊炒菜。
比一個人做飯效率提高了,比多個人減少了人力,但是一個人的能力是有限的。
“Busy”模式的 Timeline 可能是這樣:
time1:淘米
time2:入鍋煮米飯
time3:湯鍋水加熱
time4:洗湯料
time5:入鍋煲湯
time6:切菜
time7:湯鍋開了,轉文火
time8:繼續切菜
time9:炒菜
time10:湯時間夠了,關火
time11:繼續炒菜
...複制代碼
做飯的每件事都可以細分為多個更小的步驟(還可以進一步細分)
- 煮飯:淘米 -> 入鍋煮 -> 盛飯
- 炒菜:洗菜 -> 切菜 -> 入鍋炒 -> 加調料 -> 裝盤
- 煲湯:洗湯料 -> 入鍋煲 -> 加調料 -> 文火 -> 盛湯
把大事情分解成多個小步驟,把每個步驟做好,最後組合起來煮飯就完成了。這個也是我們平時使用的方式,或者接近的方式。
假如一個人的速度快十倍、一百倍、一萬倍 …… 這個效果就會更加的明顯。
Dinner & Event Loop
每個步驟之間往往需要一定時間的等待,這個時候人是閑下來的,可以去做其它事情。比如把米放入電飯鍋後,電飯鍋會自動煮,煮熟後會有提示;把湯料入鍋後,看着手表過一段時間再換溫火。一頓飯就是一個人在這樣一個個步驟之間切換完成的。
電飯鍋的提示、手表到了某個時間(定時器)等都是事件,收到事件後就可以進行下一個步驟(做出相應的處理)。做飯就是在這樣不斷處理各種事件,這就是 Event Loop。
Server
HTTP 伺服器是常見的伺服器類型,往往需要同時處理大量的請求。 伺服器對請求的處理一般分為三個階段:1.Receive;2.Handle;3.Return。
假如分别使用這三種方式處理請求:
- 一個線程處理完一個請求再處理下一個請求
- 多個線程每個線程處理一個請求
- 一個請求同時處理多個請求
Serialization
單線程串行,一個個請求順序處理:處理請求1 -> 處理請求2 -> 處理請求 X。
簡單,資源消耗少。但是資源使用率不高,效率低。如果阻塞會導緻等待,并發能力低。
Parallellism
一個線程處理一個請求:A 線程處理請求 1,B 線程處理請求 2,N 線程處理請求 X。
複雜,并發性高,但是消耗大量資源,還有線程切換成本。這個是 Java Web 常用的模式。
Concurrency
一個線程同時處理多個請求:
Time1 處理請求 1
Time2 處理請求 2
Time3 處理請求1
Time4 處理請求 2
TimeN 處理請求 X複制代碼
略複雜,并發性高,資源消耗低,效率高,但是一個線程的處理能力有限。通過 I/O Multiplexing 實作,比如 Select、Epoll 等。這個 Nginx 和 Node.js 用的模式。
“Concurrency” 模式的 timeline 可能是這樣:
time1:建立請求1
time2:建立請求2
time3:等待請求1資料
time4:等待請求2資料
time5:讀取請求1資料
time6:處理請求1
time7:讀取請求2資料
time8:傳回請求1
...複制代碼
處理請求的三個階段可以進一步細分為更多小的步驟
- Receive:建立連接配接 -> 等待可讀 -> 接收資料1 -> 接收資料 N (可能需要多次讀取) -> 資料接收完成
- Handle:可能進行很多的操作,讀寫資料庫、檔案、壓縮等。每個又會繼續分為更多的小步驟,比如讀取資料庫會有網絡 I/O
- Return:等待可寫 -> 寫入資料1 -> 寫書資料 N (可能多次寫入) -> 資料寫入完成 -> 關閉連接配接
把請求分解成多個步驟,把每個步驟完成,最後組合起來,就完成了一個請求處理。
Server & Event Loop
每個步驟之間往往需要一定時間的等待,這個時間線程是閑下來的,可以去做其它事情。比如在等待讀取請求資料的時候,可以去處理其它請求。CPU、記憶體、網卡等之間的速度差異是非常大的,等待的時間 CPU 可以做的事情是很明顯的。
建立連接配接、資料接收完成等都是事件,收到事件後就會進行下一個步驟(做出相應的處理)。伺服器就是在這樣不斷處理各種事件,這個就是 Event Loop。
Why Event Loop
每個人隻有一個大腦、兩隻手、兩條腿,我們每天隻有 24 小時,是以如何高效的做事情尤為重要。生活或工作中我們往往可以同時應對多任務,我們可以輕松同時應對煮飯、炒菜、煲湯等事情。
對于 CPU 來說,CPU 的計算能力也是一定的,也就是每個時間段可以做的事情也是有限的,是以如何高效的利用 CPU 尤為重要。
無論是做飯還是伺服器處理用戶端請求,我們都在把每個任務拆解成很多小的步驟,每個步驟之間都有時間差,我們利用時間差可以同時做多個任務。是以 EL 本質上是個體高效處理多任務的一種方式。
CPU 時間片分給線程,目的是為了可以同時處理多任務。但是多線程帶來了線程資源的消耗以及線程切換帶來的時間片損耗。如果我們可以在一個線程同時處理多個任務,那麼就可以避免這個問題,并且保持 CPU 的高效利用。I/O 多路複用讓單線程可以同時處理多網絡請求,同時 CPU 的速度遠遠大于網絡速度,明顯的時間差,是以使用 EL 模型處理多網絡請求是一種比較高效的的方式。我們熟知的高性能網絡應用 Nginx 和 Redis 都使用了 EL。
