摘要:定時器階段這個是事件循環(huán)開始的階段,綁定到這個階段的隊列,保留著定時器的回調(diào),盡管它并沒有將回調(diào)推入隊列中,但是以最小的堆來維持計時器并且在到達(dá)規(guī)定的事件后執(zhí)行回調(diào)。
本文,將會詳細(xì)的講解 node.js 事件循環(huán)工作流程和生命周期
一些常見的誤解最常見的誤解之一,事件循環(huán)是 Javascript 引擎(V8,spiderMonkey等)的一部分。事實上事件循環(huán)主要利用 Javascript 引擎來執(zhí)行代碼。
首先沒有棧,其次這個過程是復(fù)雜的,有多個隊列(像數(shù)據(jù)結(jié)構(gòu)中的隊列)參與。但是大多數(shù)開發(fā)者知道多少有的回調(diào)函數(shù)被推進(jìn)一個單一的隊列里面,是完全錯誤的。
由于錯誤的 node.js 事件循環(huán)圖,我們中有一部分人認(rèn)為u有兩個線程。一個執(zhí)行 Javascript,另一個執(zhí)行事件循環(huán)。事實上都在一個線程里面運(yùn)行。
另一個非常大的誤解是 setTimeout 的回調(diào)函數(shù)在給定的延遲完成之后被(可能是 OS 或者 內(nèi)核)推進(jìn)一個隊列。
作為常見的事件循環(huán)描述只有一個隊列;所以一些開發(fā)者認(rèn)為 setImmediate 將回調(diào)放在工作隊列的前面。這是完全錯誤的,在 Javascript 的工作隊列都是先進(jìn)先出的。
事件循環(huán)的架構(gòu)在我們開始描述事件循環(huán)的工作流程時,知道它的架構(gòu)非常重要。下圖為事件循環(huán)真正的工作流程:
圖中不同的盒子代表不同的階段,每個階段執(zhí)行特定的工作。每個階段都有一個隊列(這里說成隊列主要是為了更好理解;真實的數(shù)據(jù)結(jié)構(gòu)可能不是隊列),Javascript 可以在任何一個階段執(zhí)行(除了 idle & prepare)。你在圖片中也能看到 nextTickQueue 和 microTaskQueue,它們不是循環(huán)的一部分,它們之中的回調(diào)可以在任意階段執(zhí)行。它們有更高的優(yōu)先級去執(zhí)行。
現(xiàn)在你知道了事件循環(huán)是不同階段和不同隊列的結(jié)合;下面是每個階段的描述。
這個是事件循環(huán)開始的階段,綁定到這個階段的隊列,保留著定時器(setTimeout, setInterval)的回調(diào),盡管它并沒有將回調(diào)推入隊列中,但是以最小的堆來維持計時器并且在到達(dá)規(guī)定的事件后執(zhí)行回調(diào)。
這個階段執(zhí)行在事件循環(huán)中 pending_queue 里的回調(diào),這些回調(diào)時被之前的操作推入的。例如當(dāng)你嘗試往 tcp 中寫入一些東西,這個工作完成了,然后回調(diào)被推入到隊列中。錯誤處理的回調(diào)也在這里。
盡管名字是空閑(idle),但是每個 tick 都運(yùn)行。Prepare 也在輪詢階段開始之前運(yùn)行。不管怎樣,這兩個階段是 node 主要做一些內(nèi)部操作的階段。
可能整個事件循環(huán)最重要的一個階段就是 poll phase。這個階段接受新傳入的連接(新的 Socket 建立等)和數(shù)據(jù)(文件讀取等)。我們可以將輪詢階段分成幾個不同的部分。
如果在 watch_queue (這個隊列被綁定到輪詢階段)有東西,它們將會被一個接著一個的執(zhí)行知道隊列為空或者系統(tǒng)達(dá)到最大的限制。
一旦隊列為空,node 就會等待新的連接。等待或者睡眠的事件取決于多種因素。
輪詢的下一個階段是 check pahse,這個專用于 setImmediate 的階段。為什么需要一個專門的隊列來處理 setImmediate 回調(diào)?這是因為輪詢階段的行為,待會兒將在流程部分討論?,F(xiàn)在只需要記住檢查(check)階段主要處理 setImmediate() 的回調(diào)。
回調(diào)的關(guān)閉(stocket.on("close", () => {}))都在這里處理的,更像一個清理階段。
nextTickQueue 中的任務(wù)保留在被 process.nextTick() 觸發(fā)的回調(diào)。 microTaskQueue 保留著被 Promise 觸發(fā)的回調(diào)。它們都不是事件循環(huán)地一部分(不是在 libUV 中開發(fā)地),而是在 node 中。在 C/C++ 和 Javascript 有交叉的時候,它們都是盡可能快地被調(diào)用。因此它們應(yīng)該在當(dāng)前操作運(yùn)行后(不一定是當(dāng)前 js 回調(diào)執(zhí)行完)。
事件循環(huán)地工作流程當(dāng)在你的控制臺運(yùn)行 node my-script.js ,node 設(shè)置事件循環(huán)然后運(yùn)行你主要的模塊(my-script.js)事件循環(huán)的外部。一旦主要模塊執(zhí)行完,node 將會檢查循環(huán)是否還活著(事件循環(huán)中是否還有事情要做)?如果沒有,將會在執(zhí)行退出回調(diào)后退出。process, on("exit", foo) 回調(diào)(退出回調(diào))。但是如果循環(huán)還活著,node 將會從計時器階段進(jìn)入循環(huán)。
事件循環(huán)進(jìn)入計時器階段并且檢查在計時器隊列中是否有需要執(zhí)行的。好吧,這句話聽起來非常簡單,但是事件循環(huán)實際上要執(zhí)行一些步驟發(fā)現(xiàn)合適的回調(diào)。實際上計時器腳本以升序儲存在堆內(nèi)存中。它首先獲取到一個執(zhí)行計時器,計算下是否 now-registeredTime == delta?如果是,他會執(zhí)行這個計時器的回調(diào)并且檢查下一個計時器。直到找到一個還沒有約定時間的計時器,它會停止檢查其他的定時器(因為定時器都以升序排好了)并且移到下一個階段了。
假設(shè)你調(diào)用了 setTimeout 4次創(chuàng)建了4個定時器,分別相對于時間 t 來說 100,200,300,400 的差值。
假設(shè)事件循環(huán)在 t+250 進(jìn)入到了計時器階段。它會首先看下計時器 A,A 的過期時間是 t+100。但是現(xiàn)在時間是 t+250。因此它將執(zhí)行綁定在計時器 A 上的回調(diào)。然后去檢查計時器 B,發(fā)現(xiàn)它的過期時間是 t+200,因此也會執(zhí)行 B 的回調(diào)?,F(xiàn)在它會檢查 C,發(fā)現(xiàn)它的過期時間是 t+300,因此將會離開它。時間循環(huán)不會去檢查 D,因為計時器是按升序拍好的;因此 D 的閾值比 C 大。然而這個階段有一個系統(tǒng)相關(guān)的硬限制,如果達(dá)到系統(tǒng)依賴最大限制數(shù)量,即使有未執(zhí)行的計時器,它也會移到下一個階段。
計時器階段后,事件循環(huán)將會進(jìn)入到了懸而未決的 I/O 階段,然后檢查一下 pengding_queue 中是否有來自于之前的懸而未決的任務(wù)的回調(diào)。如果有,一個接一個的執(zhí)行,直到隊列為空,或者達(dá)到系統(tǒng)的最大限制。之后,事件循環(huán)將會移到 idle handler 階段,其次是準(zhǔn)備階段做一些內(nèi)部的操作。然后最終可能進(jìn)入到最重要的階段 poll phase。
像名字說的那樣,這是一個觀察的階段。觀察是否有新的請求或者連接傳入。當(dāng)事件循環(huán)進(jìn)入輪詢階段,它會在 watcher_queue 中執(zhí)行腳本,包含文件讀響應(yīng),新的 socket 或者 http 連接請求,直到事件耗盡或者像其他階段那樣達(dá)到系統(tǒng)依賴上限。假設(shè)沒有要執(zhí)行的回調(diào),輪詢在某些特定的條件下將會等待一會兒。如果在檢查隊列(check queue),懸而未決隊列(pending queue),或者關(guān)閉隊列(closing callbacks queue 或者 idle handler queue)里面有任何任務(wù)等待,它將等待 0 毫秒。然后它會根據(jù)定時器堆來決定等待時間執(zhí)行第一個定時器(如果可獲取)。如果第一個定時器閾值經(jīng)過了,毫無疑問它不需要等待(就會執(zhí)行第一個定時器)。
輪詢階段結(jié)束之后,立即來到檢查階段。這個階段的隊列中有被 api setImmediate 觸發(fā)的回調(diào)。它將會像其他階段那樣一個接著一個的執(zhí)行,直到隊列為空或者達(dá)到依賴系統(tǒng)的最大限制。
完成在檢查階段的任務(wù)之后,事件循環(huán)的下一個目的地是處理關(guān)閉或者銷毀類型的回調(diào) close callback。事件循環(huán)執(zhí)行完這個階段的隊列中的回調(diào)后,它會檢查循環(huán)(loop)是否還活著,如果沒有,退出。但是如果還有工作要做,它會進(jìn)入下一個循環(huán);因此在計時器階段。如果你認(rèn)為之前例子中的定時器(A & B)過期,那么現(xiàn)在定時器階段將會從定時器 C 開始檢查是否過期。
因此,這兩個隊列的回調(diào)函數(shù)什么時候運(yùn)行?它們當(dāng)然在從當(dāng)前階段到下一個階段之前盡可能快的運(yùn)行。不像其他階段,它們兩個沒有系統(tǒng)依賴的醉倒限制,node 運(yùn)行它們直到兩個隊列是空的。然而,nextTickQueue 會比 microTaskQueue 有著更高的任務(wù)優(yōu)先級。
我從 Javascript 開發(fā)者哪里聽到普遍的一個詞就是 ThreadPool。一個普遍的誤解是,nodejs 有一個處理所有異步操作的進(jìn)程池。但是實際上進(jìn)程池是 libUV (nodejs用來處理異步的第三方庫)庫中的。之所以沒有在圖中畫出來,是因為它不是循環(huán)機(jī)制的一部分。目前,并不是每個異步任務(wù)都會被進(jìn)程池處理的。libUV 能夠靈活使用操作系統(tǒng)的異步 api 來保持環(huán)境為事件驅(qū)動。然而操作系統(tǒng)的 api 不能做文件讀取,dns 查詢等,這些由進(jìn)程池來處理,默認(rèn)只有 4 個進(jìn)程。你可以通過設(shè)置 uv_threadpool_size 的環(huán)境變量增加進(jìn)程數(shù)直到 128.
帶有示例的工作流程希望你能理解事件循環(huán)是如何工作的。C 語言 中同步的 while 幫助 Javascript 成為異步的。每次只處理一件事但是很吶阻塞。當(dāng)然,無論我們?nèi)绻枋隼碚摚詈玫睦斫膺€是示例,因此,讓我們通過一些代碼片段來理解這個腳本。
setTimeout(() => {console.log("setTimeout"); }, 0); setImmediate(() => {console.log("setImmediate"); });
你能夠猜到上面的輸出嗎?好吧,你可能認(rèn)為 setTimeout 會先被打印出來,但是不能保證,為什么呢?執(zhí)行完主模塊之后進(jìn)入計時器階段,他可能不會或者會發(fā)現(xiàn)你的計時器耗盡了。為什么呢?一個計時器腳本是根據(jù)系統(tǒng)時間和你提供的增量時間注冊的。setTimeout 調(diào)用的同時,計時器腳本被寫入到了內(nèi)存中,根據(jù)你的機(jī)器性能和其他運(yùn)行在它上面的操作(不是node)的不同,可能會有一個很小的延遲。另一點(diǎn)時,node僅僅在進(jìn)入計時器階段(每一輪遍歷)之前設(shè)置一個變量 now,將 now 作為當(dāng)前時間。因此你可以說相當(dāng)于精確的時間有點(diǎn)問題。這就是不確定性的原因。如果你在一個計時器代碼的回調(diào)里面指向相同的代碼會得到相同的結(jié)果。
然而,如果你移動這段代碼到 i/o 周期里,保證 setImmediate 回調(diào)會先于 setTimeout 運(yùn)行。
fs.readFile("my-file-path.txt", () => { setTimeout(() => {console.log("setTimeout");}, 0); setImmediate(() => {console.log("setImmediate");}); });
var i = 0; var start = new Date(); function foo () { i++; if (i < 1000) { setImmediate(foo); } else { var end = new Date(); console.log("Execution time: ", (end - start)); } } foo();
上面的例子非常簡單。調(diào)用函數(shù) foo 函數(shù)內(nèi)部再通過 setImmediate 遞歸調(diào)用 foo 直到 1000。在我的電腦上面,大概花費(fèi)了 6 到 8 毫秒。仙子啊修改下上面的代碼,把 setImmedaite(foo) 換成 setTimeout(foo, o)。
var i = 0; var start = new Date(); function foo () { i++; if (i < 1000) { setTimeout(foo, 0); } else { var end = new Date(); console.log("Execution time: ", (end - start)); } } foo();
現(xiàn)在在我的電腦上面運(yùn)行這段代碼花費(fèi)了 1400+ms。為什么會這樣?它們都沒有 i/o 事件,應(yīng)該一樣才對。上面兩個例子等待事件是 0.為什么花費(fèi)這么長時間?通過事件比較找到了偏差,CPU 密集型任務(wù),花費(fèi)更多的時間。注冊計時器腳本也花費(fèi)事件。定時器的每個階段都需要做一些操作來決定一個定時器是否應(yīng)該執(zhí)行。長時間的執(zhí)行也會導(dǎo)致更多的 ticks。然而,在 setImmediate 中,只有檢查這一個階段,就好像在一個隊列里面然后執(zhí)行就行了。
var i = 0; function foo(){ i++; if (i>20) return; console.log("foo"); setTimeout(()=>console.log("setTimeout"), 0); process.nextTick(foo); } setTimeout(foo, 2000);
你認(rèn)為上面輸出是什么?是的,它會輸出 foo 然后輸出 setTimeout。2秒后被 nextTickQueue 遞歸調(diào)用 foo() 打印出第一個 foo。當(dāng)所有的 nextTickQueue 執(zhí)行完了,開始執(zhí)行其他(比如 setTimeout 回調(diào))的。
所以是每個回調(diào)執(zhí)行完之后,開始檢查 nextTickQueue 的嗎? 我們改下代碼看下。
var i = 0; function foo(){ i++; if (i>20) return; console.log("foo"); setTimeout(()=>console.log("setTimeout"), 0); process.nextTick(foo); } setTimeout(foo, 2000); setTimeout(()=>{console.log("Other setTimeout"); }, 2000);
在 setTimeout 之后,我僅僅用一樣的延遲時間添加了另一個輸出 Other setTimeout 的 setTimeout。盡管不能保證,但是有可能會在輸出第一個 foo 之后輸出 Other setTimeout 。相同的定時器分為一個組,nextTickQueue 會在正在進(jìn)行中的回調(diào)組執(zhí)行完之后執(zhí)行。
一些普遍的問題就像我們大多數(shù)人都認(rèn)為事件循環(huán)是在一個多帶帶的線程里面,將回調(diào)推入一個隊列,然后一個接著一個執(zhí)行。第一次讀到這篇文章的讀者可能會感到疑惑,Javascript 在哪里執(zhí)行的?正如我早些時候說的,只有一個線程,來自于本身使用 V8 或者其他引擎的事件循環(huán)的 Javascript 代碼也是在這里運(yùn)行的。執(zhí)行是同步的,如果當(dāng)前的 Javascript 執(zhí)行還沒有完成,事件循環(huán)不會傳播。
首先不是0,而是1.當(dāng)你設(shè)置一個計時器,時間為小于 1,或者大于 2147483647ms 的時候,它會自動設(shè)置為 1.因此你如果設(shè)置 setTimeout 的延遲時間為 0,它會自動設(shè)置為1.
此外,setImmediate 會減少額外的檢查。因此 setImmediate 會執(zhí)行的更快一些。它也放置在輪詢階段之后,因此來自于任何一個到來的請求 setImmediate 回調(diào)將會立即被執(zhí)行。
setImmediate 和 process.nextTick() 都命名錯了。所以功能上,setImmediate 在下一個 tick 執(zhí)行,nextTick 是馬上執(zhí)行的。
由于 nextTickQueue 沒有回調(diào)執(zhí)行的限制。因此如果你遞歸地執(zhí)行 process.nextTick(),你地程序可能永遠(yuǎn)在事件循環(huán)中出不來,無論你在其他階段有什么。
它可能會初始化計時器,但回調(diào)可能永遠(yuǎn)不會被調(diào)用。因為如果 node 在 exit callback 階段,它已經(jīng)跳出事件循環(huán)了。因此沒有回去執(zhí)行。
一些短地結(jié)論事件循環(huán)沒有工作棧
事件循環(huán)不在一個多帶帶地線程里面,Javascript 的執(zhí)行也不是像從隊列中彈出一個回調(diào)執(zhí)行那么簡單。
setImmediate 沒有將回調(diào)推入到工作隊列地頭部,有一個專門的階段和隊列。
setImmediate 在下一個循環(huán)執(zhí)行,nextTick 實際上是馬上執(zhí)行。
當(dāng)心,如果遞歸調(diào)用的話,nextTickQueue 可能會阻塞你的 node 代碼。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/108190.html
摘要:輪詢投票處理下一次處理的新事件立即設(shè)置運(yùn)行通過注冊的所有回調(diào)關(guān)閉執(zhí)行所有的回調(diào)工作處理延遲此度量標(biāo)準(zhǔn)測量線程池處理異步任務(wù)需要多長時間。高工作時間處理延遲表明繁忙耗盡的線程池。 原文=> What you should know to really understand the Node.js Event Loop Node.js 是一個基于事件的平臺。這就意味著在Node中發(fā)生的所...
摘要:變量的說法來自于,這是在多線程模型下出現(xiàn)并發(fā)問題的一種解決方案。目前已經(jīng)有庫實現(xiàn)了應(yīng)用層棧幀的可控編碼,同時可以在該棧幀存活階段綁定相關(guān)數(shù)據(jù),我們便可以利用這種特性實現(xiàn)類似多線程下的變量。 ThreadLocal變量的說法來自于Java,這是在多線程模型下出現(xiàn)并發(fā)問題的一種解決方案。ThreadLocal變量作為線程內(nèi)的局部變量,在多線程下可以保持獨(dú)立,它存在于線程的生命周期內(nèi),可以在...
摘要:前端每周清單第期現(xiàn)狀分析與優(yōu)化策略單元測試爬蟲作者王下邀月熊編輯徐川前端每周清單專注前端領(lǐng)域內(nèi)容,以對外文資料的搜集為主,幫助開發(fā)者了解一周前端熱點(diǎn)分為新聞熱點(diǎn)開發(fā)教程工程實踐深度閱讀開源項目巔峰人生等欄目。 showImg(https://segmentfault.com/img/remote/1460000011008022); 前端每周清單第 29 期:Web 現(xiàn)狀分析與優(yōu)化策略...
摘要:需要校驗字節(jié)信息是否符合規(guī)范,避免惡意信息和不規(guī)范數(shù)據(jù)危害運(yùn)行安全。具有相同哈希值的鍵值對會組成鏈表。通過在協(xié)議下添加了一層協(xié)議對數(shù)據(jù)進(jìn)行加密從而保證了安全。常見的非對稱加密包括等。 類加載過程 Java 中類加載分為 3 個步驟:加載、鏈接、初始化。 加載。 加載是將字節(jié)碼數(shù)據(jù)從不同的數(shù)據(jù)源讀取到JVM內(nèi)存,并映射為 JVM 認(rèn)可的數(shù)據(jù)結(jié)構(gòu),也就是 Class 對象的過程。數(shù)據(jù)源可...
閱讀 3734·2023-04-25 17:45
閱讀 3441·2021-09-04 16:40
閱讀 1009·2019-08-30 13:54
閱讀 2143·2019-08-29 12:59
閱讀 1413·2019-08-26 12:11
閱讀 3287·2019-08-23 15:17
閱讀 1531·2019-08-23 12:07
閱讀 3891·2019-08-22 18:00