【純乾貨】Node.js eventloop + 線程池源碼分析(建議細看)

前言

Coding 應當是一輩子的事業,而不只僅是 30 歲的青春飯
本文已收錄 GitHub https://github.com/ponkans/F2E,裏面有一線大廠進階指南,歡迎 Star,持續更新前端

《大前端進階 Node.js》系列 異步非阻塞(上)中,怪怪帶你們看清了異步非阻塞這個渣女,講了不少以前你們可能沒有想清楚的概念細節。node

這一期,咱們迴歸 Node 的異步 IO 模型,開始以前,先提出幾個問題,本文也將圍繞這幾個問題展開 xio 習。git

  • 線程池是什麼?
  • 線程池如何實現線程複用?
  • eventloop 是什麼?
  • Node 如何基於線程池和 eventloop 實現異步非阻塞?

線程池

線程池是個啥

先開啓第一個問題,何爲線程池。一項技術的誕生每每是爲了解決某個問題,那線程池也不例外。github

咱們先來假設一個場景。面試

假如你在某紀佳緣網上班,你的老闆讓你開發一個推薦服務,作的事情很簡單,當有用戶訪問你的服務的時候,你須要根據用戶的一些特性(好比性別)給他推薦對象。好比怪怪在訪問的時候,你就須要推薦富婆安全

當你接到這個任務,首先要想到的就是每一個用戶過來的請求應該是個獨立的線程。由於用戶和用戶之間是隔離的,而每一個線程作的事情就是一系列推薦操做。微信

固然,這難不倒優秀的你,你能夠在每一個用戶請求的時候新建一個線程,在請求響應結束後銷燬它,一切都很美好。app

燃鵝,由於你推薦的太到位了,大家網站火了,一羣富婆爭着來註冊。這個時候問題也就來了,若是 1000 個富婆同時點擊,那意味着 1000 個請求會過來,你是否是就要創建 1000 個線程呢?異步

在這一系列請求結束後,這些線程是否是又會被銷燬?線程建立最直觀的開銷就是內存,這樣的頻繁建立和銷燬對性能的影響顯而易見,同時這樣的設計並不能撐其瞬時峯值流量工具

由於這樣的設計,富婆們得不到知足,某紀佳緣朝不保夕。

這時,線程池應運而生。以前講過,一項技術的誕生永遠是爲了解決某些問題,那線程池解決了什麼問題呢?總結下其實就是線程的生命週期管控。下面咱們來細細分析。

線程生命週期管控

先看咱們的問題,頻繁的建立和銷燬線程。解決的辦法是啥呢?必然是線程複用

一個線程被建立以後,即便這一次響應結束了,也不讓他被回收,下一次請求來的時候依然讓他去處理。

這裏很關鍵的點在於如何讓一個線程不被回收

看似很神奇,一個線程執行完了操做還能繼續存在?這麼持久?作法其實很簡單,寫一個死循環便可。線程一直處於循環中,當有請求來的時候處理請求,當沒有的時候就一直等待,等到了再執行處理,處理完再等待,反覆橫跳,無限循環。

這裏引伸出了第二個關鍵點,處於死循環中的線程怎麼知道啥時候有請求要給他處理

這裏不一樣語言實現方式不徹底相同,但大同小異,本質上必定是基於阻塞喚醒。當沒有任務的時候,全部線程處於阻塞狀態,當任務來的時候,空閒線程去競爭這個任務,取到的線程開始執行,未取到的繼續阻塞。

這裏你們可能網上看過行行色色的線程池解讀,但要深入理解的話,仍是要從源碼入手。(在源碼面前,一切的花裏胡哨都蒼白無力)

先給出libuv threadPool 源碼地址:源碼地址

咱們直接截取線程池實現的核心部分,以下圖裏的註釋,基本實現符合預期。

線程池源碼1
線程池源碼1
線程池源碼2
線程池源碼2

簡單說明下,上面代碼裏 uv_cond_signal 等同於喚醒阻塞線程,uv_cond_wait 等同於讓當前線程進入阻塞狀態。

線程池總結

最後總結下就是,線程池利用死循環讓線程沒法結束,在等待任務期間處於阻塞狀態,利用阻塞喚醒來讓線程接收任務(本質上阻塞喚醒基於信號量),從而達到線程複用,結束當前任務後進入下一次循環,周而復始。

eventloop

eventloop 是個啥

eventloop 的含義如同其名字同樣:事件循環。

說的通俗一點其實就是一個 while(true)循環,循環裏面作的事情就是不斷的 check 有沒有待處理的任務,若是有就處理任務,若是沒有就繼續下一次循環。

大體流程以下圖。

eventloop 的思想很簡單,他並不關心你的回調如何實現,IO 操做什麼時候結束。
他作的事情就是不斷的去取事件,取到了就執行。那這裏有一個關鍵的點就是他在哪裏取的 event 呢?

答案是:watcher。每一個事件循環中都會有觀察者,每輪循環都會去觀察者中拿事件,而後執行。其實這個所謂的 watcher 就是一個用來存放事件的queue(隊列)。

怎麼來理解這個 watcher 呢,深刻淺出 nodejs 裏面給了一個很形象的比喻。


在餐廳裏,前臺小妹每每負責記錄客人的點菜,廚師在後廚作菜。小妹在拿到客人的菜單後會把菜單放到廚房,而廚師只須要不斷的看菜單,作菜,再看菜單,作菜。他並不關心是誰點的菜,也不關心這個菜在何時點的。

這裏這個放菜單的地方就是那個 watcher,本質上是一個 queue,廚師就如同 eventloop,不斷的處於作菜的循環中,每一輪循環會去取 queue 裏面的請求,若是有回調就執行回調,沒有的話進入下一輪循環。

eventloop + 線程池 = 異步非阻塞

上面比較詳細的講解了線程池和 eventpoll,接下來咱們來看一下如何用其來實現異步非阻塞。

咱們來一步一步捋清思路。首先,可愛的你發起了一個 IO 調用,從 《大前端進階 Node.js》系列 異步非阻塞中講過,一個 IO 調用要麼是阻塞調用,要麼是先非阻塞的發起 IO,再在須要看結果的時候阻塞的去獲取,顯然這兩種模式都不是咱們想要的。

咱們要的是異步非阻塞,因此這個 IO 調用必定不是在主線程中執行,這個時候咱們就能聯想到上面的線程池。

主線程不能被阻塞,但線程池裏面的線程能夠,主線程只須要把 IO 調用交給線程池來執行,本身就能夠愉快的玩耍,以此達到了咱們的第一個目標:非阻塞

異步呢?如何讓線程池裏面的調用在結束的時候去執行回調?這個時候 eventloop 閃亮登場。

在線程池 IO 處理結束後,會主動的把結束的請求放入 eventloop 的觀察者(watcher)中,也就是咱們的 queue 中,eventloop 處於不斷循環的狀態,當下一次循環 check 到 queue 裏有請求的時候,就會取出來而後執行回調,這樣咱們想要的異步就達到了。

最終經過線程池和 eventloop 結合,呈現出的效果就是,當你發起一次 IO 調用,你無需阻塞的等待 IO 結束,也無需在想利用 IO 結果的時候不斷的輪詢,整個 IO 過程對主線程而言非阻塞,而且自動結束時執行回調,達到咱們想要的異步非阻塞。

最後,咱們引用一張《深刻淺出 Nodejs》裏的圖。

異步非阻塞
異步非阻塞

如上圖,在發起異步調用後,會封裝一個請求參數,裏面會包括參數和結束時要執行的回調。

這個request(請求參數)封裝好後會扔給線程池執行,線程池裏面的線程若是有空閒,就會在線程池的 queue 中去取這個 request 並執行 IO 操做。

在執行結束以後通知IOCP,其實就是把這個 requeat 放入一個 queue,這個隊列就是線程池和事件循環之間的樞紐。

事件循環在循環的時候發現隊列裏面有請求,就會取出來並執行相應的回調,一次完美的異步非阻塞就此完成。

總結

本文已收錄 Github https://github.com/ponkans/F2E,裏面有一線大廠進階指南,歡迎 Star,持續更新

仔細的看完怪怪的 Node 異步非阻塞(上)(下)兩個系列,還不能吊打面試題你儘管來找我~

libuv threadPool 源碼都帶你看過了,還不明白,就真的說不過去了!!

最後,精闢的完美總結以下。


核心總結:Node 利用線程池來執行 IO 調用,避免阻塞主線程,執行結束後把結果和請求放入一個隊列,利用事件循環來取出隊列的請求,最後執行回調,達到了異步非阻塞的效果。

近期原創熱文傳送門,biubiu~:

喜歡的小夥伴加個關注,點個贊哦,感恩💕😊

聯繫我 / 公衆號

微信搜索【接水怪】或掃描下面二維碼回覆」加羣「,我會拉你進技術交流羣。講真的,在這個羣,哪怕您不說話,光看聊天記錄也是一種成長。(阿里技術專家、敖丙做者、Java3y、蘑菇街資深前端、螞蟻金服安全專家、各路大牛都在)。

接水怪也會按期原創,按期跟小夥伴進行經驗交流或幫忙看簡歷。加關注,不迷路,有機會一塊兒跑個步🏃 ↓↓↓

相關文章
相關標籤/搜索