摘要:異步在中,是在單線程中執(zhí)行的沒錯,但是內部完成工作的另有線程池,使用一個主進程和多個線程來模擬異步。在事件循環(huán)中,觀察者會不斷的找到線程池中已經完成的請求對象,從中取出回調函數(shù)和數(shù)據(jù)并執(zhí)行。
1. 介紹
單線程編程會因阻塞I/O導致硬件資源得不到更優(yōu)的使用。多線程編程也因為編程中的死鎖、狀態(tài)同步等問題讓開發(fā)人員頭痛。
Node在兩者之間給出了它的解決方案:利用單線程,遠離多線程死鎖、狀態(tài)同步等問題;利用異步I/O,讓單線程遠離阻塞,以好使用CPU。
實際上,node只是在應用層屬于單線程,底層其實通過libuv維護了一個阻塞I/O調用的線程池。
但是:在應用層面,JS是單線程的,業(yè)務代碼中不能存在耗時過長的代碼,否則可能會嚴重拖后續(xù)代碼(包括回調)的處理。如果遇到需要復雜的業(yè)務計算時,應當想辦法啟用獨立進程或交給其他服務進行處理。
1.1 異步I/O在Node中,JS是在單線程中執(zhí)行的沒錯,但是內部完成I/O工作的另有線程池,使用一個主進程和多個I/O線程來模擬異步I/O。
當主線程發(fā)起I/O調用時,I/O操作會被放在I/O線程來執(zhí)行,主線程繼續(xù)執(zhí)行下面的任務,在I/O線程完成操作后會帶著數(shù)據(jù)通知主線程發(fā)起回調。
事件循環(huán)是Node的執(zhí)行模型,正是這種模型使得回調函數(shù)非常普遍。
在進程啟動時,Node便會創(chuàng)建一個類似while(true)的循環(huán),執(zhí)行每次循環(huán)的過程就是判斷有沒有待處理的事件,如果有,就取出事件及其相關的回調并執(zhí)行他們,然后進入下一個循環(huán)。如果不再有事件處理,就退出進程。
Event loop是一種程序結構,是實現(xiàn)異步的一種機制。Event loop可以簡單理解為:
所有任務都在主線程上執(zhí)行,形成一個執(zhí)行棧(execution context stack)。
主線程之外,還存在一個"任務隊列"(task queue)。系統(tǒng)把異步任務放到"任務隊列"之中,然后主線程繼續(xù)執(zhí)行后續(xù)的任務。
一旦"執(zhí)行棧"中的所有任務執(zhí)行完畢,系統(tǒng)就會讀取"任務隊列"。如果這個時候,異步任務已經結束了等待狀態(tài),就會從"任務隊列"進入執(zhí)行棧,恢復執(zhí)行。
主線程不斷重復上面的第三步。
Node中事件循環(huán)階段解析:
┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
每個階段都有一個FIFO的回調隊列(queue)要執(zhí)行。而每個階段有自己的特殊之處,簡單說,就是當event loop進入某個階段后,會執(zhí)行該階段特定的(任意)操作,然后才會執(zhí)行這個階段的隊列里的回調。當隊列被執(zhí)行完,或者執(zhí)行的回調數(shù)量達到上限后,event loop會進入下個階段。
Phases Overview 階段總覽
timers: 這個階段執(zhí)行setTimeout()、setInterval()設定的回調。
I/O callbacks: 執(zhí)行幾乎所有的回調,除了close callbacks、setTimeout()、setInterval()、setImmediate()的回調。
idle, prepare: 僅內部使用。
poll: 獲取新的I/O事件;node會在適當條件下阻塞在這里。
check: 執(zhí)行setImmediate()設定的回調。
close callbacks: 執(zhí)行比如socket.on("close", ...)的回調。
1. timers一個timer指定一個下限時間而不是準確時間,定時器setTimeout()和setInterval()在達到這個下限時間后執(zhí)行回調。在指定的時間過后,timers會盡早的執(zhí)行回調,但是系統(tǒng)調度或者其他回調的執(zhí)行可能會延遲它們。
從技術上來說,poll階段控制timers什么時候執(zhí)行,而執(zhí)行的具體位置在timers。
下限的時間有一個范圍:[1, 2147483647],如果設定的時間不在這個范圍,將被設置為1。
執(zhí)行除了close callbacks、setTimeout()、setInterval()、setImmediate()回調之外幾乎所有回調,比如說TCP連接發(fā)生錯誤。
3. idle, prepare系統(tǒng)內部的一些調用。
4. poll這是最復雜的一個階段。poll會檢索新的I/O events,并且會在合適的時候阻塞,等待回調被加入。
poll階段有兩個主要的功能:一是執(zhí)行下限時間已經達到的timers的回調,一是處理poll隊列里的事件。
注:Node很多API都是基于事件訂閱完成的,這些API的回調應該都在poll階段完成。
當事件循環(huán)進入poll階段:
poll隊列不為空的時候,事件循環(huán)肯定是先遍歷隊列并同步執(zhí)行回調,直到隊列清空或執(zhí)行回調數(shù)達到系統(tǒng)上限。
poll隊列為空的時候,這里有兩種情況。
如果代碼已經被setImmediate()設定了回調,那么事件循環(huán)直接結束poll階段進入check階段來執(zhí)行check隊列里的回調。
如果代碼沒有被設定setImmediate()設定回調:
如果有被設定的timers,那么此時事件循環(huán)會檢查timers,如果有一個或多個timers下限時間已經到達,那么事件循環(huán)將繞回timers階段,并執(zhí)行timers的有效回調隊列。
如果沒有被設定timers,這個時候事件循環(huán)是阻塞在poll階段等待事件回調被加入poll隊列。
Node的很多API都是基于事件訂閱完成的,比如fs.readFile,這些回調應該都在poll階段完成。
5. checksetImmediate()在這個階段執(zhí)行。
這個階段允許在poll階段結束后立即執(zhí)行回調。如果poll階段空閑,并且有被setImmediate()設定的回調,那么事件循環(huán)直接跳到check執(zhí)行而不是阻塞在poll階段等待poll 事件們 (poll events)被加入。
注意:如果進行到了poll階段,setImmediate()具有最高優(yōu)先級,只要poll隊列為空且注冊了setImmediate(),無論是否有timers達到下限時間,setImmediate()的代碼都先執(zhí)行。
6. close callbacks如果一個socket或handle被突然關掉(比如socket.destroy()),close事件將在這個階段被觸發(fā),否則將通過process.nextTick()觸發(fā)。
1.3 請求對象對于Node中的異步I/O調用而言,回調函數(shù)不由開發(fā)者來調用,從JS發(fā)起調用到I/O操作完成,存在一個中間產物,叫請求對象。
在JS發(fā)起調用后,JS調用Node的核心模塊,核心模塊調用C++內建模塊,內建模塊通過libuv判斷平臺并進行系統(tǒng)調用。在進行系統(tǒng)調用時,從JS層傳入的方法和參數(shù)都被封裝在一個請求對象中,請求對象被放在線程池中等待執(zhí)行。JS立即返回繼續(xù)后續(xù)操作。
在線程可用時,線程會取出請求對象來執(zhí)行I/O操作,執(zhí)行完后將結果放在請求對象中,并歸還線程。
在事件循環(huán)中,I/O觀察者會不斷的找到線程池中已經完成的請求對象,從中取出回調函數(shù)和數(shù)據(jù)并執(zhí)行。
跑完當前執(zhí)行環(huán)境下能跑完的代碼。每一個事件消息都被運行直到完成為止,在此之前,任何其他事件都不會被處理。這和C等一些語言不通,它們可能在一個線程里面,函數(shù)跑著跑著突然停下來,然后其他線程又跑起來了。JS這種機制的一個典型的壞處,就是當某個事件處理耗時過長時,后面的事件處理都會被延后,直到這個事件處理結束,在瀏覽器環(huán)境中運行時,可能會出現(xiàn)某個腳本運行時間過長,頁面無響應的提示。Node環(huán)境則可能出現(xiàn)大量用戶請求被掛起,不能及時響應的情況。
2. 非I/O的異步APINode中除了異步I/O之外,還有一些與I/O無關的異步API,分別是:setTimeout()、setInterval()、process.nextTick()、setImmediate(),他們并不是像普通I/O操作那樣真的需要等待事件異步處理結束再進行回調,而是出于定時或延遲處理的原因才設計的。
2.1 setTimeout()與setInterval()這兩個方法實現(xiàn)原理與異步I/O相似,只不過不用I/O線程池的參與。
使用它們創(chuàng)建的定時器會被放入timers隊列的一個紅黑樹中,每次事件循環(huán)執(zhí)行時會從相應隊列中取出并判斷是否超過定時時間,超過就形成一個事件,回調立即執(zhí)行。
所以,和瀏覽器中一樣,這個并不精確,會被長時間的同步事件阻塞。
值得一提的是,在Node的setTimeout的源碼中:
// Node源碼 after *= 1; // coalesce to number or NaN if (!(after >= 1 && after <= TIMEOUT_MAX)) { if (after > TIMEOUT_MAX) { process.emitWarning(...); } after = 1; // schedule on next tick, follows browser behavior }
意思是如果沒有設置這個after,或者小于1,或者大于TIMEOUT_MAX(2^31-1),都會被強制設置為1ms。也就是說setTimeout(xxx,0)其實等同于setTimeout(xxx,1)。
2.2 setImmediate()setImmediate()是放在check階段執(zhí)行的,實際上是一個特殊的timer,跑在event loop中一個獨立的階段。它使用libuv的API來設定在 poll 階段結束后立即執(zhí)行回調。
來看看這個例子:
setTimeout(function() { console.log("setTimeout") }, 0) setImmediate(function() { console.log("setImmediate") }) // 輸出不穩(wěn)定
setTimeout與setImmediate先后入隊之后,首先進入的是timers階段,如果我們的機器性能一般或者加入了一個同步長耗時操作,那么進入timers階段,1ms已經過去了,那么setTimeout的回調會首先執(zhí)行。
如果沒有到1ms,那么在timers階段的時候,超時時間沒到,setTimeout回調不執(zhí)行,事件循環(huán)來到了poll階段,這個時候隊列為空,此時有代碼被setImmediate(),于是先執(zhí)行了setImmediate()的回調函數(shù),之后在下一個事件循環(huán)再執(zhí)行setTimemout的回調函數(shù)。
setTimeout(function() { console.log("set timeout") }, 0) setImmediate(function() { console.log("set Immediate") }) for (let i = 0; i < 100000; i++) {} // 可以保證執(zhí)行時間超過1ms // 穩(wěn)定輸出: setTimeout setImmediate
這樣就可以穩(wěn)定輸出了。
再一個栗子:
const fs = require("fs") fs.readFile("./filePath.js", (err, data) => { setTimeout(() => console.log("setTimeout") , 0) setImmediate(() => console.log("setImmediate")) console.log("開始了") for (let i = 0; i < 100000; i++) {} }) // 輸出 開始了 setImmediate setTimeout
這里我們就會發(fā)現(xiàn),setImmediate永遠先于setTimeout執(zhí)行。
fs.readFile的回調是在poll階段執(zhí)行的,當其回調執(zhí)行完畢之后,setTimeout與setImmediate先后入了timers與check的隊列,繼續(xù)到poll,poll隊列為空,此時發(fā)現(xiàn)有setImmediate,于是事件循環(huán)先進入check階段執(zhí)行回調,之后在下一個事件循環(huán)再在timers階段中執(zhí)行setTimeout回調,雖然這個setTimeout已經到了超時時間。
再來個栗子:
同樣的,這段代碼也是一樣的道理:
setTimeout(() => { setImmediate(() => console.log("setImmediate") ); setTimeout(() => console.log("setTimeout") , 0); }, 0);
以上的代碼在timers階段執(zhí)行外部的setTimeout回調后,內層的setTimeout和setImmediate入隊,之后事件循環(huán)繼續(xù)往后面的階段走,走到poll階段的時候發(fā)現(xiàn)隊列為空,此時有代碼被setImmedate(),所以直接進入check階段執(zhí)行響應回調(注意這里沒有去檢測timers隊列中是否有成員到達超時事件,因為setImmediate()優(yōu)先)。之后在下一個事件循環(huán)的timers階段中再去執(zhí)行相應的回調。
2.3 process.nextTick()與Promise對于這兩個,我們可以把它們理解成一個微任務。也就是說,它們其實不屬于事件循環(huán)的一部分。
有時我們想要立即異步執(zhí)行一個任務,可能會使用延時為0的定時器,但是這樣開銷很大。我們可以換而使用process.nextTick(),它會將傳入的回調放入nextTickQueue隊列中,下一輪Tick之后取出執(zhí)行,不管事件循環(huán)進行到什么地步,都在當前執(zhí)行棧的操作結束的時候調用,參見Nodejs官網。
process.nextTick方法指定的回調函數(shù),總是在當前執(zhí)行隊列的尾部觸發(fā),多個process.nextTick語句總是一次執(zhí)行完(不管它們是否嵌套),遞歸調用process.nextTick,將會沒完沒了,主線程根本不會去讀取事件隊列,導致阻塞后續(xù)調用,直至達到最大調用限制。
相比于在定時器中采用紅黑樹樹的操作時間復雜度為0(lg(n)),而process.nextTick()的時間復雜度為0(1),相比之下更高效。
來舉一個復雜的栗子,這個栗子搞懂基本上就全部理解了:
setTimeout(() => { process.nextTick(() => console.log("nextTick1")) setTimeout(() => { console.log("setTimout1") process.nextTick(() => { console.log("nextTick2") setImmediate(() => console.log("setImmediate1")) process.nextTick(() => console.log("nextTick3")) }) setImmediate(() => console.log("setImmediate2")) process.nextTick(() => console.log("nextTick4")) console.log("sync2") setTimeout(() => console.log("setTimout2"), 0) }, 0) console.log("sync1") }, 0) // 輸出: sync1 nextTick1 setTimout1 sync2 nextTick2 nextTick4 nextTick3 setImmediate2 setImmediate1 setTimout22.4 結論
process.nextTick(),效率最高,消費資源小,但會阻塞CPU的后續(xù)調用;
setTimeout(),精確度不高,可能有延遲執(zhí)行的情況發(fā)生,且因為動用了紅黑樹,所以消耗資源大;
setImmediate(),消耗的資源小,也不會造成阻塞,但效率也是最低的。
網上的帖子大多深淺不一,甚至有些前后矛盾,在下的文章都是學習過程中的總結,如果發(fā)現(xiàn)錯誤,歡迎留言指出~
參考:
Node——異步I/O
Node探秘之事件循環(huán)
Node探秘之事件循環(huán)--setTimeout/setImmediate/process.nextTick的差別
細說setTimeout/setImmediate/process.nextTick的區(qū)別
深入淺出Nodejs
Node官方文檔
由setTimeout和setImmediate執(zhí)行順序的隨機性窺探Node的事件循環(huán)機制
Node.js的event loop及timer/setImmediate/nextTick
Node.js 探秘:初識單線程的 Node.js | Taobao FED | 淘寶前端團隊
Node.js 事件循環(huán)機制 - 一像素 - 博客園
PS:歡迎大家關注我的公眾號【前端下午茶】,一起加油吧~
另外可以加入「前端下午茶交流群」微信群,長按識別下面二維碼即可加我好友,備注加群,我拉你入群~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/90536.html
摘要:回調函數(shù)是在異步操作完成后傳播其操作結果的函數(shù),總是用來替代同步操作的返回指令。下面的圖片顯示了中事件循環(huán)過程當異步操作完成時,執(zhí)行權就會交給這個異步操作開始的地方,即回調函數(shù)。 本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書筆記,在GitHub連載更新,同步翻譯版鏈接。 歡迎關注我的專欄,之后的博文將在專欄同步: Enc...
摘要:的單線程,與它的用途有關。特點的顯著特點異步機制事件驅動。隊列的讀取輪詢線程,事件的消費者,的主角。它將不同的任務分配給不同的線程,形成一個事件循環(huán),以異步的方式將任務的執(zhí)行結果返回給引擎。 這兩天跟同事同事討論遇到的一個問題,js中的event loop,引出了chrome與node中運行具有setTimeout和Promise的程序時候執(zhí)行結果不一樣的問題,從而引出了Nodejs的...
摘要:為什么要避免阻塞事件循環(huán)和工作池使用少量線程來處理許多客戶端,在中有兩種類型的線程一個事件循環(huán)又稱主循環(huán)主線程事件線程等,以及一個工作池也稱為線程池中的個的池。 不要阻塞事件循環(huán)(或工作池) 你應該閱讀這本指南嗎? 如果你編寫的內容比簡短的命令行腳本更復雜,那么閱讀本文應該可以幫助你編寫性能更高、更安全的應用程序。 本文檔是在考慮Node服務器的情況下編寫的,但這些概念也適用于復雜的N...
摘要:接下來的部分將討論如何確保事件循環(huán)和工作池的公平調度。不要阻塞事件循環(huán)事件循環(huán)通知每個新客戶端連接并協(xié)調對客戶端的響應。 你應該閱讀本指南嗎? 如果您編寫比命令行腳本更復雜的程序,那么閱讀本文可以幫助您編寫性能更高,更安全的應用程序。 在編寫本文檔時,主要是基于Node服務器。但里面的原則也適用于其它復雜的Node應用程序。在沒有特別說明操作系統(tǒng)的情況下,默認為Linux。 TL; D...
摘要:簡介項目命名為就是一個服務器單純開發(fā)一個服務器的想法,變成構建網絡應用的一個基本框架發(fā)展為一個強制不共享任何資源的單線程,單進程系統(tǒng)。單線程弱點無法利用多核錯誤會引起整個應用退出,應用的健壯性大量計算占用導致無法繼續(xù)調用異步。 NodeJs簡介 Ryan Dahl項目命名為:web.js 就是一個Web服務器.單純開發(fā)一個Web服務器的想法,變成構建網絡應用的一個基本框架.Node發(fā)展...
閱讀 861·2023-04-26 00:11
閱讀 2658·2021-11-04 16:13
閱讀 2106·2021-09-09 09:33
閱讀 1477·2021-08-20 09:35
閱讀 3823·2021-08-09 13:42
閱讀 3611·2019-08-30 15:55
閱讀 1056·2019-08-30 15:55
閱讀 2221·2019-08-30 13:55