解秘 Node.js 单线程实现高并发请求原理,以及串联同步执行并发请求的方案
最近在做一個支持多進程請求的 Node 服務,要支持多并發請求,而且請求要按先后順序串聯同步執行返回結果。
對,這需求就是這么奇琶,業務場景也是那么奇琶。
需求是完成了,為了對 Node.js 高并發請求原理有更深一些的理解,特意寫一篇文章來鞏固一下相關的知識點。
問題
Node.js 由這些關鍵字組成:事件驅動、非阻塞I/O、高效、輕量。
于是在我們剛接觸 Node.js 時,會有所疑問:
為什么在瀏覽器中運行的 JavaScript 能與操作系統進行如此底層的交互?
Node 真的是單線程嗎?
如果是單線程,他是如何處理高并發請求的?
Node 事件驅動是如何實現的?
下來我們一起來解秘這是怎么一回事!
架構一覽
上面的問題,都挺底層的,所以我們從 Node.js 本身入手,先來看看 Node.js 的結構。
Node.js 標準庫,這部分是由 Javascript編寫的,即我們使用過程中直接能調用的 API。在源碼中的 lib 目錄下可以看到。
Node bindings,這一層是 Javascript 與底層 C/C++ 能夠溝通的關鍵,前者通過 bindings 調用后者,相互交換數據。
第三層是支撐 Node.js 運行的關鍵,由 C/C++ 實現。
V8:Google 推出的 Javascript VM,也是 Node.js 為什么使用的是 JavaScript 的關鍵,它為 JavaScript 提供了在非瀏覽器端運行的環境,它的高效是 Node.js 之所以高效的原因之一。
Libuv:它為 Node.js 提供了跨平臺,線程池,事件池,異步 I/O 等能力,是 Node.js 如此強大的關鍵。
C-ares:提供了異步處理 DNS 相關的能力。
http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、數據壓縮等其他的能力。
單線程、異步
單線程:所有任務需要排隊,前一個任務結束,才會執行后一個任務。如果前一個任務耗時很長,后一個任務就不得不一直等著。Node 單線程指的是 Node 在執行程序代碼時,主線程是單線程。
異步:主線程之外,還維護了一個"事件隊列"(Event queue)。當用戶的網絡請求或者其它的異步操作到來時,Node 都會把它放到 Event Queue 之中,此時并不會立即執行它,代碼也不會被阻塞,繼續往下走,直到主線程代碼執行完畢。
注:
JavaScript 是單線程的,Node 本身其實是多線程的,只是 I/O 線程使用的 CPU 比較少;還有個重要的觀點是,除了用戶的代碼無法并行執行外,所有的 I/O (磁盤 I/O 和網絡 I/O) 則是可以并行起來的。
libuv 線程池默認打開 4 個,最多打開 128 個 線程。
事件循環
Nodejs 所謂的單線程,只是主線程是單線程。
主線程運行 V8 和 JavaScript
多個子線程通過?事件循環?被調度
可以抽象為:主線程對應于老板,正在工作。一旦發現有任務可以分配給職員(子線程)來做,將會把任務分配給底下的職員來做。同時,老板繼續做自己的工作,等到職員(子線程)把任務做完,就會通過事件把結果回調給老板。老板又不停重復處理職員(子線程)子任務的完成情況。
老板(主線程)給職員(子線程)分配任務,當職員(子線程)把任務做完之后,通過事件把結果回調給老板。老板(主線程)處理回調結果,執行相應的 JavaScript。
更具體的解釋請看下圖:
1、每個 Node.js 進程只有一個主線程在執行程序代碼,形成一個執行棧(execution context stack)。
2、Node.js 在主線程里維護了一個"事件隊列"(Event queue),當用戶的網絡請求或者其它的異步操作到來時,Node 都會把它放到 Event Queue之中,此時并不會立即執行它,代碼也不會被阻塞,繼續往下走,直到主線程代碼執行完畢。
3、主線程代碼執行完畢完成后,然后通過 Event Loop,也就是事件循環機制,檢查隊列中是否有要處理的事件,這時要分兩種情況:如果是非 I/O 任務,就親自處理,并通過回調函數返回到上層調用;如果是 I/O 任務,就從 線程池 中拿出一個線程來處理這個事件,并指定回調函數,當線程中的 I/O 任務完成以后,就執行指定的回調函數,并把這個完成的事件放到事件隊列的尾部,線程歸還給線程池,等待事件循環。當主線程再次循環到該事件時,就直接處理并返回給上層調用。這個過程就叫 事件循環 (Event Loop)。
4、期間,主線程不斷的檢查事件隊列中是否有未執行的事件,直到事件隊列中所有事件都執行完了,此后每當有新的事件加入到事件隊列中,都會通知主線程按順序取出交 Event Loop 處理。
優缺點
Nodejs 的優點:I/O 密集型處理是 Nodejs 的強項,因為 Nodejs 的 I/O 請求都是異步的(如:sql 查詢請求、文件流操作操作請求、http 請求...)
Nodejs 的缺點:不擅長 cpu 密集型的操作(復雜的運算、圖片的操作)
總結
1、Nodejs 與操作系統交互,我們在 JavaScript 中調用的方法,最終都會通過 process.binding 傳遞到 C/C++ 層面,最終由他們來執行真正的操作。Node.js 即這樣與操作系統進行互動。
2、Nodejs 所謂的單線程,只是主線程是單線程,所有的網絡請求或者異步任務都交給了內部的線程池去實現,本身只負責不斷的往返調度,由事件循環不斷驅動事件執行。
3、Nodejs 之所以單線程可以處理高并發的原因,得益于 libuv 層的事件循環機制,和底層線程池實現。
4、Event loop 就是主線程從主線程的事件隊列里面不停循環的讀取事件,驅動了所有的異步回調函數的執行,Event loop 總共 7 個階段,每個階段都有一個任務隊列,當所有階段被順序執行一次后,event loop 完成了一個 tick。
參考文章:Nodejs探秘:深入理解單線程實現高并發原理
串聯同步執行并發請求
就像上面說的:Node.js 在主線程里維護了一個"事件隊列"(Event queue),當用戶的網絡請求或者其它的異步操作到來時,Node 都會把它放到 Event Queue之中,此時并不會立即執行它,代碼也不會被阻塞,繼續往下走,直到主線程代碼執行完畢。
所以要串聯同步執行并發請求的關鍵在于維護一個隊列,隊列的特點是 先進先出,按隊列里面的順序執行就可以達到串聯同步執行并發請求的目的。
方案
根據每個請求的 uniqueId 變量作為唯一令牌
隊列里面維護一個結果數組和一個執行隊列,把執行隊列完成的 令牌與結果 存儲在結果數組里面
根據唯一令牌,一直去獲取執行完成的結果,間隔 200 毫秒,超時等待時間為 10 分鐘
一直等待并獲取結果,等待到有結果時,才返回給請求;并根據令牌把結果數組里面相應的項刪除
隊列
代碼:
class?Recorder?{private?list:?any[];private?queueList:?any[];private?intervalTimer;constructor()?{this.list?=?[];this.queueList?=?[];this.intervalTimer?=?null;}//?根據?id?獲取任務結果public?get(id:?string)?{let?data;console.log('this.list:?',?this.list);let?index;for?(let?i?=?0;?i?<?this.list.length;?i++)?{const?item?=?this.list[i];if?(id?===?item.id)?{data?=?item.data;index?=?i;break;}}//?刪除獲取到結果的項if?(index?!==?undefined)?{this.list.splice(index,?1);}return?data;}public?clear()?{this.list?=?[];this.queueList?=?[];}//?添加項public?async?addQueue(item:?any)?{this.queueList.push(item);}public?async?runQueue()?{clearInterval(this.intervalTimer);if?(!this.queueList.length)?{//?console.log('隊列執行完畢');return;}//?取出隊列里面的最后一項const?item?=?this.queueList.shift();console.log('item:?',?item);//?執行隊列的回調const?data?=?await?item.callback();console.log('回調執行完成:?',?data);//?把結果放進?結果數組this.list.push({?id:?item.id,?data?});}public?interval()?{clearInterval(this.intervalTimer);this.intervalTimer?=?setInterval(async?()?=>?{clearInterval(this.intervalTimer);//?一直執行里面的任務await?this.runQueue();this.interval();},?200);} }const?recorder?=?new?Recorder(); recorder.interval();export?default?recorder;服務
下面模擬一個請求端口的的 Node 服務。
代碼:
const?Koa?=?require('koa') const?Router?=?require('koa-router') const?cuid?=?require('cuid'); const?bodyParser?=?require('koa-bodyparser') import?recorder?from?"./libs/recorder";const?MAX_WAITING_TIME?=?60?*?5;?//?最大等待時長 //?web服務端口 const?SERVER_PORT:?number?=?3000; const?app?=?new?Koa(); app.use(bodyParser()); const?router?=?new?Router();/***?程序睡眠*?@param?time?毫秒*/ const?timeSleep?=?(time:?number)?=>?{return?new?Promise((resolve)?=>?{setTimeout(()?=>?{resolve("");},?time);}); };/***?程序睡眠*?@param?second?秒*/ const?sleep?=?(second:?number)?=>?{return?timeSleep(second?*?1000); };router.post("/getPort",?async?(ctx,?next)?=>?{const?{?num?}?=?ctx.request.body;const?uniqueId?=?cuid();console.log('uniqueId:?',?uniqueId);recorder.addQueue({id:?uniqueId,callback:?getPortFun(num)});let?waitTime?=?0;while?(!ctx.body)?{await?sleep(0.2);console.log('1');const?data:?any?=?recorder.get(uniqueId);if?(data)?{ctx.body?=?{code:?0,data:?data,msg:?'success'};}waitTime++;//?超過最大時間就返回一個結果if?(waitTime?>?MAX_WAITING_TIME)?{ctx.body?=?{};}} });//?返回一個函數 function?getPortFun(num)?{return?()?=>?{return?new?Promise((resolve)?=>?{//?模擬異步程序setTimeout(()?=>?{console.log(`num${num}:?`,?num);resolve(num?*?num);},?num?*?1000);});}; }app.use(router.routes()).use(router.allowedMethods());app.listen(SERVER_PORT);最后
最近狀態很差勁,上班工作多人的時候還好,但是自己一個人的時候,心情常常不能平靜,心好亂,有點心慌 ????
心情不好時,啥都不想干,心態有點扭轉不過來,集中不了注意力,所以最近想專心寫篇原創技術文章都不行,想重構自己開源的 blog 項目也不行,很糟糕 ????
所以最近的原創技術文章有點難產了 ????
心態急需調整,周末想出去玩,放松一下自己,找回那個斗志滿滿的真我才行,唉。
可以加貓哥的 wx:CB834301747?,一起閑聊前端。
微信搜 “前端GitHub”,回復 “電子書” 即可以獲得?160?本前端精華書籍哦。
往期精文
10 個 GitHub 上超火的前端面試項目,打造自己的加薪寶庫!
Vue3 的學習教程匯總、源碼解釋項目、支持的 UI 組件庫、優質實戰項目
總結
以上是生活随笔為你收集整理的解秘 Node.js 单线程实现高并发请求原理,以及串联同步执行并发请求的方案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Web 趋势榜:上周最热门的 10 大
- 下一篇: Web 趋势榜:上周不可错过的最热门的