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