Node探秘之事件循環(huán)(2)--setTimeout/setImmediate/process.nextTick的差別

前言


根據(jù)上一篇文章,我們可知球订,node對(duì)回調(diào)事件的處理完全是基于事件循環(huán)的tick的后裸,因此具有幾大特征:

1、在應(yīng)用層面冒滩,JS是單線程的微驶,業(yè)務(wù)代碼中不能存在耗時(shí)過長(zhǎng)的代碼,否則可能會(huì)嚴(yán)重拖后續(xù)代碼(包括回調(diào))的處理开睡。如果遇到需要復(fù)雜的業(yè)務(wù)計(jì)算時(shí)因苹,應(yīng)當(dāng)想辦法啟用獨(dú)立進(jìn)程或交給其他服務(wù)進(jìn)行處理。

2篇恒、回調(diào)是不精確容燕,因?yàn)榍懊娴脑颍瑂etTimeout并不能得到準(zhǔn)確的超時(shí)回調(diào)婚度。

3蘸秘、不同類型的觀察者,處理的優(yōu)先級(jí)不同蝗茁,idle觀察者最先醋虏,I/O觀察者其次,check觀察者最后哮翘。

那么本文主要要分析的是基于tick的幾個(gè)主要回調(diào)實(shí)現(xiàn)颈嚼,setTimeout/setInterval/process.nextTick/setImmediate,這幾個(gè)屬于js異步回調(diào)的比較特殊的饭寺,因?yàn)樗麄儾⒉皇窍衿胀↖/O操作那樣真的需要等待事件異步處理結(jié)束再進(jìn)行回調(diào)阻课,而是出于定時(shí)或延遲處理的原因才設(shè)計(jì)的。分析起來相對(duì)簡(jiǎn)單艰匙,因此我們就從它們?nèi)胧窒奚罚鸩浇议_事件循環(huán)的秘密。

區(qū)別及源碼分析


◇setTimeout/setInterval

setTimeout和setInterval的表現(xiàn)和實(shí)現(xiàn)其實(shí)基本相同员凝,不同的只是setInterval會(huì)不斷重復(fù)署驻。在底層實(shí)現(xiàn)上他們是創(chuàng)建了一個(gè)Timeout的中間對(duì)象,并且放到了實(shí)現(xiàn)定時(shí)器的紅黑樹中,每一次tick開始時(shí)旺上,都會(huì)到這個(gè)紅黑樹中檢查是否存在超時(shí)的回調(diào)瓶蚂,如果存在,則一一按照超時(shí)順序取出來進(jìn)行回調(diào)宣吱。因此窃这,我們可以得出這樣一個(gè)結(jié)論:

js的定時(shí)器是不可靠的。因此單線程的原因征候,它是基于tick的钦听,每次tick開始時(shí)才開始檢查是否有超時(shí),如果一個(gè)tick耗時(shí)過長(zhǎng)倍奢,在它之后出發(fā)的定時(shí)回調(diào)都將被延遲朴上。因此才會(huì)出現(xiàn)像“問題1”這樣的情況。

setTimeout第二個(gè)參數(shù)設(shè)置為0或者不設(shè)置卒煞,意思不是立即執(zhí)行回調(diào)痪宰,而是在下次tick時(shí)立即執(zhí)行(當(dāng)然,實(shí)際上畔裕,這里有點(diǎn)小問題衣撬,后面會(huì)講到)!這setTimeout也解釋了Promise的實(shí)現(xiàn)中扮饶,resolve方法里為什么有些要用setTimeout(..., 0)具练,這是為了解決在碰到同步代碼時(shí),resolve先于then執(zhí)行的問題甜无。但是它有一個(gè)嚴(yán)重的問題扛点,就是回調(diào)依然被送入定時(shí)器的紅黑樹,存在一定的性能問題岂丘。因此陵究,通常大家會(huì)用process.nextTick()或setImmediate()來替代它。


lib/timers.js

這里先創(chuàng)建了一個(gè)Timeout對(duì)象奥帘,然后調(diào)用active函數(shù)使他生效

lib/timer.js

這里調(diào)用insert方法把當(dāng)前Timeout對(duì)象插入到了一個(gè)地方

lib/timer.js

這個(gè)insert方法比較有意思铜邮,list是一個(gè)Timer對(duì)象,通過調(diào)用它的start方法可以使定時(shí)器生效寨蹋,同時(shí)它又是個(gè)雙向鏈表松蒜,這iterm就是被插入到了這個(gè)雙向鏈表中。這是為什么已旧?

其實(shí)秸苗,代碼里面已經(jīng)給出了解釋

原來因?yàn)閷?shí)際開發(fā)過程中,經(jīng)常會(huì)出現(xiàn)很多的socket會(huì)被設(shè)置為相同的超時(shí)時(shí)間评姨,如果為每一個(gè)這樣的超時(shí)請(qǐng)求都設(shè)置一個(gè)watcher难述,那就太浪費(fèi)系統(tǒng)資源了萤晴,系統(tǒng)負(fù)載也會(huì)變得很高吐句,性能變差胁后。因?yàn)椋@里用了一個(gè)非常巧妙的方法嗦枢,那就是把超時(shí)時(shí)間相同的Timeout對(duì)象都扔到同一個(gè)鏈表中攀芯,然后再把這個(gè)Timer鏈表作為一個(gè)獨(dú)立的超時(shí)單位啟動(dòng)。

src/timer_wrap.cc

這里調(diào)用了uv_timer_start(不同系統(tǒng)實(shí)現(xiàn)方式不同文虏,這里的源碼是unix的)

deps/uv/src/unix/timer.cc

原來這個(gè)uv_timer_start其實(shí)主要就是把這個(gè)Timer對(duì)象插入到了一顆紅黑樹上侣诺。

如果還記得我上文對(duì)事件循環(huán)的代碼分析的話,你一定會(huì)注意在事件循環(huán)的while中氧秘,有uv__run_timers這一行年鸳,通過上面這段代碼,就能看出來這個(gè)uv__run_timers就是從紅黑樹上取下所有超時(shí)的Timer對(duì)象丸相,然后依次調(diào)用他們的回調(diào)方法進(jìn)行回調(diào)搔确。

◇process.nextTick

實(shí)際上,process.nextTick()方法的操作相對(duì)較為輕量灭忠,每次調(diào)用Process.nextTick()方法膳算,只會(huì)將回調(diào)函數(shù)放入隊(duì)列中,在下一輪Tick時(shí)取出執(zhí)行弛作。定時(shí)器采用紅黑樹的操作時(shí)間復(fù)雜度為o(lg(n))涕蜂,而nextTick()的時(shí)間復(fù)雜度為o(1)。相較之下映琳,process.nextTick()更高效机隙。

src/node.js

由以上代碼可知,nextTick函數(shù)萨西,會(huì)將callback封裝為一個(gè)obj對(duì)象黍瞧,并且插入到nextTickQueue隊(duì)列(數(shù)組)中。


src/node.js

由上述代碼可知原杂,每次nextTick回調(diào)印颤,都會(huì)nextTickQueue數(shù)組中的回調(diào)全部跑完!

◇setImmediate


lib/timers.js

setImmediate函數(shù)穿肄,首先把callback封裝成了一個(gè)immediate對(duì)象年局,然后把它插入到了immediateQueue隊(duì)列(數(shù)組)中。

注意上面的那句process._immediateCallback = processImmediate咸产,這行代碼就是把process._immediateCallback設(shè)置成了processImmediate的別名矢否,下次tick的時(shí)候就會(huì)調(diào)用這個(gè)函數(shù)進(jìn)行回調(diào)。


setImmediate()方法和process.nextTick()方法十分類似脑溢,都是將回調(diào)函數(shù)延遲在下一次立即執(zhí)行僵朗。setImmediate是創(chuàng)建了一個(gè)叫為immediate的中間對(duì)象赖欣,并且放入到了immediateQueue隊(duì)列中在Node v0.9.1之前,setImmediate()還沒有實(shí)現(xiàn)验庙,那時(shí)候?qū)崿F(xiàn)類似的功能主要是通過process.nextTick()來完成顶吮。

但兩者之間其實(shí)是有差別的。區(qū)別表現(xiàn)為兩點(diǎn):

1粪薛、process.nextTick中回調(diào)函數(shù)的優(yōu)先級(jí)高于setImmediate悴了,根據(jù)我前面寫的那篇文章可知,原因在于事件循環(huán)對(duì)觀察者的檢查是有先后順序的违寿,process.nextTick屬于idle觀察者湃交,setImmediate屬于check觀察者。在每一輪循環(huán)檢查中藤巢,idle觀察者先于I/O觀察者搞莺,I/O觀察者先于check觀察者。

而且掂咒,這里最有意思的是下面這段代碼的執(zhí)行結(jié)果才沧,大家以為會(huì)是什么樣的輸出?

他的實(shí)際輸出是:

nextTick 1

nextTick 2

timeout

immediate

上面代碼中表明俏扩,由于process.nextTick方法指定的回調(diào)函數(shù)糜工,總是在當(dāng)前"執(zhí)行棧"的尾部觸發(fā),所以不僅函數(shù)A比setTimeout指定的回調(diào)函數(shù)timeout先執(zhí)行录淡,而且函數(shù)B也比timeout先執(zhí)行捌木。這說明,如果有多個(gè)process.nextTick語(yǔ)句(不管它們是否嵌套)嫉戚,將全部在當(dāng)前"執(zhí)行棧"執(zhí)行刨裆。這里具體為什么這樣,其實(shí)我現(xiàn)在也搞不懂彬檀,以后有機(jī)會(huì)可以慢慢在讀讀代碼帆啃,如果有知道的朋友,可以告訴我一下窍帝,謝謝了努潘。

我們由此得到了一個(gè)重要區(qū)別:多個(gè)process.nextTick語(yǔ)句總是一次執(zhí)行完,多個(gè)setImmediate則需要多次才能執(zhí)行完坤学。事實(shí)上疯坤,這正是Node.js 10.0版添加setImmediate方法的原因,否則像下面這樣的遞歸調(diào)用process.nextTick深浮,將會(huì)沒完沒了压怠,主線程根本不會(huì)去讀取"事件隊(duì)列"封孙!

由于process.nextTick指定的回調(diào)函數(shù)是在本次"事件循環(huán)"觸發(fā)崇败,而setImmediate指定的是在下次"事件循環(huán)"觸發(fā)澎语,所以很顯然坑匠,前者總是比后者發(fā)生得早,而且執(zhí)行效率也高(因?yàn)椴挥脵z查"任務(wù)隊(duì)列")雨让。

2雇盖、在實(shí)現(xiàn)上,process.nextTick的回調(diào)函數(shù)保存在一個(gè)數(shù)組中宫患,setImmediate則保存在一個(gè)鏈表中刊懈。順便這里拋出一個(gè)樸靈老師在《深入淺出Node.js》中對(duì)process.nextTick和setImmediate的不夠準(zhǔn)確的描述:“在行為上这弧,process.nextTick在每輪循環(huán)中將數(shù)組中的回調(diào)函數(shù)全部執(zhí)行完娃闲,而setImmediate在每輪循環(huán)中執(zhí)行鏈表中的一個(gè)回調(diào)函數(shù)∝依耍” 并且用了一段代碼進(jìn)行作證:

樸靈老師書里面說的結(jié)果是:

正常執(zhí)行

nextTick延遲執(zhí)行1

nextTick延遲執(zhí)行2

setImmediate延遲執(zhí)行1

強(qiáng)勢(shì)插入

setImmediate延遲執(zhí)行2

但我跑出來的真實(shí)結(jié)果卻是:

正常執(zhí)行

nextTick延遲執(zhí)行1

nextTick延遲執(zhí)行2

setImmediate延遲執(zhí)行1

setImmediate延遲執(zhí)行2

強(qiáng)勢(shì)插入

我相信樸老師一定是驗(yàn)證過那段代碼的皇帮,也就是說當(dāng)時(shí)他測(cè)試應(yīng)該是正確的。為了印證為什么我測(cè)試的結(jié)果為什么跟樸老師給的結(jié)果存在差異蛋辈,我做了兩件事情属拾,一是在不同的node版本下運(yùn)行這段代碼(樸老師寫那本書的時(shí)候,node最新版本為0.10.13冷溶,而我的版本是4.2.4)渐白,二是去翻閱node的源碼實(shí)現(xiàn),通過底層原理來描述這件事情逞频。

首先纯衍,我測(cè)試了在不同版本下node運(yùn)行的差異:

通過這個(gè)測(cè)試,我們可以發(fā)現(xiàn)苗胀,從設(shè)計(jì)邏輯出發(fā)襟诸,setImmediate每次只執(zhí)行鏈表中的一個(gè)回調(diào)應(yīng)該是早期node版本中是一個(gè)bug,這在后面的版本中修復(fù)了基协。所以歌亲,才出現(xiàn)了樸老師的書里描述的結(jié)果跟實(shí)際測(cè)試的不同的現(xiàn)象。

然后澜驮,我分別對(duì)比了node在這兩個(gè)版本下的代碼的差異:

0.10.13版本的

lib/timers.js

根據(jù)以上代碼可知陷揪,在0.10.13的代碼中,每次tick處理immediate時(shí)杂穷,只會(huì)取一個(gè)回調(diào)出來進(jìn)行處理

4.x版本的

lib/timers.js

根據(jù)以上代碼可知悍缠,在4.x版本的代碼中,每次tick處理immediate時(shí)亭畜,會(huì)使用while循環(huán)扮休,把所有的immediate回調(diào)取出來依次進(jìn)行處理。

3拴鸵、setImmediate可以使用clearImmediate清除(沒搞懂這個(gè)到底能干嗎玷坠,誰(shuí)明白請(qǐng)告訴我一下)蜗搔,process.nextTick不能被清除

觀察者優(yōu)先級(jí)

在每次輪訓(xùn)檢查中,各觀察者的優(yōu)先級(jí)分別是:

idle觀察者 > I/O觀察者 > check觀察者八堡。

idle觀察者:process.nextTick

I/O觀察者:一般性的I/O回調(diào)樟凄,如網(wǎng)絡(luò),文件兄渺,數(shù)據(jù)庫(kù)I/O等

check觀察者:setImmediate缝龄,setTimeout

上面的結(jié)果顯示timeout1甚至優(yōu)于immediate執(zhí)行,原因應(yīng)該在于距離下次tick啟動(dòng)至檢查定時(shí)器的時(shí)間超過了10ms挂谍,因此timeout1那個(gè)時(shí)候其實(shí)已經(jīng)超時(shí)了叔壤。

說到這里,順便談個(gè)問題口叙。知乎上曾有人貼過一段關(guān)于setImmediate和setTimeout(xxx,0)的代碼炼绘,得出了一個(gè)這樣的結(jié)論:“而在執(zhí)行setImmedia時(shí),setTimeout是隨機(jī)的插入在setImmediate的順序中的”妄田。我對(duì)這個(gè)結(jié)論是持懷疑態(tài)度的俺亮,一個(gè)像node這樣穩(wěn)定健壯的系統(tǒng)是不太可能允許這種不可控的隨機(jī)性的,我們回過頭去看前面的代碼疟呐,發(fā)現(xiàn)了這樣一行:

lib/timers.js

意思很明顯脚曾,如果沒有設(shè)置這個(gè)after,或者小于1启具,或者大于TIMEOUT_MAX(2^31-1)本讥,都會(huì)被強(qiáng)制設(shè)置為1ms。也就是說setTimeout(xxx,0)其實(shí)等同于setTimeout(xxx,1)富纸。

那就很容易理解知乎這位作者的給出的代碼為什么是這樣的結(jié)果了囤踩。因此:setTimeout的優(yōu)先級(jí)高于setImmediate,但是因?yàn)閟etTimeout的after被強(qiáng)制修正為1晓褪,這就可能存在下一個(gè)tick觸發(fā)時(shí)堵漱,耗時(shí)尚不足1ms,setTimeout的回調(diào)依然未超時(shí)涣仿,因此setImmediate就先執(zhí)行了勤庐!這可以通過在本次tick中加入一段耗時(shí)較長(zhǎng)的代碼來來保證本次tick耗時(shí)必須超過1ms來檢測(cè):

測(cè)試顯示:不論運(yùn)行多少次,得出的結(jié)果都一樣好港,都是如下:

由此可知愉镰,setTimeout是優(yōu)先于setImmediate被處理的。

總結(jié)


要想真正理解很多why的問題钧汹,光看大量的案例和看文字解釋其實(shí)還是很難理解的丈探,死記硬背也比較難記住。最好的方法還是通過閱讀底層代碼實(shí)現(xiàn)拔莱,并思考為什么這樣設(shè)計(jì)碗降,應(yīng)該就會(huì)好很多隘竭。這些代碼分析并不完整,我個(gè)人的理解也不是非常深入讼渊,很多地方地方可能都沒有講清楚动看。以后應(yīng)該還會(huì)有更多的文章出來進(jìn)行分析。

通過上面的分析爪幻,我也簡(jiǎn)單給出幾個(gè)結(jié)論:

優(yōu)先級(jí)順序:process.nextTick > setTimeout/setInterval > setImmediate

setTimeout需要使用紅黑樹菱皆,且after設(shè)置為0,其實(shí)會(huì)被node強(qiáng)制轉(zhuǎn)換為1挨稿,存在性能上的問題仇轻,建議替換為setImmediate

process.nextTick有一些比較難懂的問題和隱患,從0.8版本開始加入setImmediate叶组,使用時(shí)拯田,建議使用setImmediate

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末历造,一起剝皮案震驚了整個(gè)濱河市甩十,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吭产,老刑警劉巖侣监,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異臣淤,居然都是意外死亡橄霉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門邑蒋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來姓蜂,“玉大人,你說我怎么就攤上這事医吊∏” “怎么了?”我有些...
    開封第一講書人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵卿堂,是天一觀的道長(zhǎng)束莫。 經(jīng)常有香客問我,道長(zhǎng)草描,這世上最難降的妖魔是什么览绿? 我笑而不...
    開封第一講書人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮穗慕,結(jié)果婚禮上饿敲,老公的妹妹穿的比我還像新娘。我一直安慰自己逛绵,他們只是感情好怀各,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開白布栗竖。 她就那樣靜靜地躺著,像睡著了一般渠啤。 火紅的嫁衣襯著肌膚如雪狐肢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,821評(píng)論 1 290
  • 那天沥曹,我揣著相機(jī)與錄音份名,去河邊找鬼。 笑死妓美,一個(gè)胖子當(dāng)著我的面吹牛僵腺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播壶栋,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼辰如,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了贵试?” 一聲冷哼從身側(cè)響起琉兜,我...
    開封第一講書人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎毙玻,沒想到半個(gè)月后豌蟋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡桑滩,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年梧疲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片运准。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡幌氮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出胁澳,到底是詐尸還是另有隱情该互,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布听哭,位于F島的核電站慢洋,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏陆盘。R本人自食惡果不足惜普筹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望隘马。 院中可真熱鬧太防,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至酿愧,卻和暖如春沥潭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嬉挡。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工钝鸽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人庞钢。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓拔恰,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親基括。 傳聞我的和親對(duì)象是個(gè)殘疾皇子颜懊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容