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

前言


根據上一篇文章,我們可知,node對回調事件的處理完全是基于事件循環的tick的,因此具有幾大特征:

1、在應用層面,JS是單線程的,業務代碼中不能存在耗時過長的代碼,否則可能會嚴重拖后續代碼(包括回調)的處理。如果遇到需要復雜的業務計算時,應當想辦法啟用獨立進程或交給其他服務進行處理。

2、回調是不精確,因為前面的原因,setTimeout并不能得到準確的超時回調。

3、不同類型的觀察者,處理的優先級不同,idle觀察者最先,I/O觀察者其次,check觀察者最后。

那么本文主要要分析的是基于tick的幾個主要回調實現,setTimeout/setInterval/process.nextTick/setImmediate,這幾個屬于js異步回調的比較特殊的,因為他們并不是像普通I/O操作那樣真的需要等待事件異步處理結束再進行回調,而是出于定時或延遲處理的原因才設計的。分析起來相對簡單,因此我們就從它們入手,逐步揭開事件循環的秘密。

區別及源碼分析


◇setTimeout/setInterval

setTimeout和setInterval的表現和實現其實基本相同,不同的只是setInterval會不斷重復。在底層實現上他們是創建了一個Timeout的中間對象,并且放到了實現定時器的紅黑樹中,每一次tick開始時,都會到這個紅黑樹中檢查是否存在超時的回調,如果存在,則一一按照超時順序取出來進行回調。因此,我們可以得出這樣一個結論:

js的定時器是不可靠的。因此單線程的原因,它是基于tick的,每次tick開始時才開始檢查是否有超時,如果一個tick耗時過長,在它之后出發的定時回調都將被延遲。因此才會出現像“問題1”這樣的情況。

setTimeout第二個參數設置為0或者不設置,意思不是立即執行回調,而是在下次tick時立即執行(當然,實際上,這里有點小問題,后面會講到)!這setTimeout也解釋了Promise的實現中,resolve方法里為什么有些要用setTimeout(..., 0),這是為了解決在碰到同步代碼時,resolve先于then執行的問題。但是它有一個嚴重的問題,就是回調依然被送入定時器的紅黑樹,存在一定的性能問題。因此,通常大家會用process.nextTick()或setImmediate()來替代它。


lib/timers.js

這里先創建了一個Timeout對象,然后調用active函數使他生效

lib/timer.js

這里調用insert方法把當前Timeout對象插入到了一個地方

lib/timer.js

這個insert方法比較有意思,list是一個Timer對象,通過調用它的start方法可以使定時器生效,同時它又是個雙向鏈表,這iterm就是被插入到了這個雙向鏈表中。這是為什么?

其實,代碼里面已經給出了解釋

原來因為實際開發過程中,經常會出現很多的socket會被設置為相同的超時時間,如果為每一個這樣的超時請求都設置一個watcher,那就太浪費系統資源了,系統負載也會變得很高,性能變差。因為,這里用了一個非常巧妙的方法,那就是把超時時間相同的Timeout對象都扔到同一個鏈表中,然后再把這個Timer鏈表作為一個獨立的超時單位啟動。

src/timer_wrap.cc

這里調用了uv_timer_start(不同系統實現方式不同,這里的源碼是unix的)

deps/uv/src/unix/timer.cc

原來這個uv_timer_start其實主要就是把這個Timer對象插入到了一顆紅黑樹上。

如果還記得我上文對事件循環的代碼分析的話,你一定會注意在事件循環的while中,有uv__run_timers這一行,通過上面這段代碼,就能看出來這個uv__run_timers就是從紅黑樹上取下所有超時的Timer對象,然后依次調用他們的回調方法進行回調。

◇process.nextTick

實際上,process.nextTick()方法的操作相對較為輕量,每次調用Process.nextTick()方法,只會將回調函數放入隊列中,在下一輪Tick時取出執行。定時器采用紅黑樹的操作時間復雜度為o(lg(n)),而nextTick()的時間復雜度為o(1)。相較之下,process.nextTick()更高效。

src/node.js

由以上代碼可知,nextTick函數,會將callback封裝為一個obj對象,并且插入到nextTickQueue隊列(數組)中。


src/node.js

由上述代碼可知,每次nextTick回調,都會nextTickQueue數組中的回調全部跑完!

◇setImmediate


lib/timers.js

setImmediate函數,首先把callback封裝成了一個immediate對象,然后把它插入到了immediateQueue隊列(數組)中。

注意上面的那句process._immediateCallback = processImmediate,這行代碼就是把process._immediateCallback設置成了processImmediate的別名,下次tick的時候就會調用這個函數進行回調。


setImmediate()方法和process.nextTick()方法十分類似,都是將回調函數延遲在下一次立即執行。setImmediate是創建了一個叫為immediate的中間對象,并且放入到了immediateQueue隊列中在Node v0.9.1之前,setImmediate()還沒有實現,那時候實現類似的功能主要是通過process.nextTick()來完成。

但兩者之間其實是有差別的。區別表現為兩點:

1、process.nextTick中回調函數的優先級高于setImmediate,根據我前面寫的那篇文章可知,原因在于事件循環對觀察者的檢查是有先后順序的,process.nextTick屬于idle觀察者,setImmediate屬于check觀察者。在每一輪循環檢查中,idle觀察者先于I/O觀察者,I/O觀察者先于check觀察者。

而且,這里最有意思的是下面這段代碼的執行結果,大家以為會是什么樣的輸出?

他的實際輸出是:

nextTick 1

nextTick 2

timeout

immediate

上面代碼中表明,由于process.nextTick方法指定的回調函數,總是在當前"執行棧"的尾部觸發,所以不僅函數A比setTimeout指定的回調函數timeout先執行,而且函數B也比timeout先執行。這說明,如果有多個process.nextTick語句(不管它們是否嵌套),將全部在當前"執行棧"執行。這里具體為什么這樣,其實我現在也搞不懂,以后有機會可以慢慢在讀讀代碼,如果有知道的朋友,可以告訴我一下,謝謝了。

我們由此得到了一個重要區別:多個process.nextTick語句總是一次執行完,多個setImmediate則需要多次才能執行完。事實上,這正是Node.js 10.0版添加setImmediate方法的原因,否則像下面這樣的遞歸調用process.nextTick,將會沒完沒了,主線程根本不會去讀取"事件隊列"!

由于process.nextTick指定的回調函數是在本次"事件循環"觸發,而setImmediate指定的是在下次"事件循環"觸發,所以很顯然,前者總是比后者發生得早,而且執行效率也高(因為不用檢查"任務隊列")。

2、在實現上,process.nextTick的回調函數保存在一個數組中,setImmediate則保存在一個鏈表中。順便這里拋出一個樸靈老師在《深入淺出Node.js》中對process.nextTick和setImmediate的不夠準確的描述:“在行為上,process.nextTick在每輪循環中將數組中的回調函數全部執行完,而setImmediate在每輪循環中執行鏈表中的一個回調函數。” 并且用了一段代碼進行作證:

樸靈老師書里面說的結果是:

正常執行

nextTick延遲執行1

nextTick延遲執行2

setImmediate延遲執行1

強勢插入

setImmediate延遲執行2

但我跑出來的真實結果卻是:

正常執行

nextTick延遲執行1

nextTick延遲執行2

setImmediate延遲執行1

setImmediate延遲執行2

強勢插入

我相信樸老師一定是驗證過那段代碼的,也就是說當時他測試應該是正確的。為了印證為什么我測試的結果為什么跟樸老師給的結果存在差異,我做了兩件事情,一是在不同的node版本下運行這段代碼(樸老師寫那本書的時候,node最新版本為0.10.13,而我的版本是4.2.4),二是去翻閱node的源碼實現,通過底層原理來描述這件事情。

首先,我測試了在不同版本下node運行的差異:

通過這個測試,我們可以發現,從設計邏輯出發,setImmediate每次只執行鏈表中的一個回調應該是早期node版本中是一個bug,這在后面的版本中修復了。所以,才出現了樸老師的書里描述的結果跟實際測試的不同的現象。

然后,我分別對比了node在這兩個版本下的代碼的差異:

0.10.13版本的

lib/timers.js

根據以上代碼可知,在0.10.13的代碼中,每次tick處理immediate時,只會取一個回調出來進行處理

4.x版本的

lib/timers.js

根據以上代碼可知,在4.x版本的代碼中,每次tick處理immediate時,會使用while循環,把所有的immediate回調取出來依次進行處理。

3、setImmediate可以使用clearImmediate清除(沒搞懂這個到底能干嗎,誰明白請告訴我一下),process.nextTick不能被清除

觀察者優先級

在每次輪訓檢查中,各觀察者的優先級分別是:

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

idle觀察者:process.nextTick

I/O觀察者:一般性的I/O回調,如網絡,文件,數據庫I/O等

check觀察者:setImmediate,setTimeout

上面的結果顯示timeout1甚至優于immediate執行,原因應該在于距離下次tick啟動至檢查定時器的時間超過了10ms,因此timeout1那個時候其實已經超時了。

說到這里,順便談個問題。知乎上曾有人貼過一段關于setImmediate和setTimeout(xxx,0)的代碼,得出了一個這樣的結論:“而在執行setImmedia時,setTimeout是隨機的插入在setImmediate的順序中的”。我對這個結論是持懷疑態度的,一個像node這樣穩定健壯的系統是不太可能允許這種不可控的隨機性的,我們回過頭去看前面的代碼,發現了這樣一行:

lib/timers.js

意思很明顯,如果沒有設置這個after,或者小于1,或者大于TIMEOUT_MAX(2^31-1),都會被強制設置為1ms。也就是說setTimeout(xxx,0)其實等同于setTimeout(xxx,1)。

那就很容易理解知乎這位作者的給出的代碼為什么是這樣的結果了。因此:setTimeout的優先級高于setImmediate,但是因為setTimeout的after被強制修正為1,這就可能存在下一個tick觸發時,耗時尚不足1ms,setTimeout的回調依然未超時,因此setImmediate就先執行了!這可以通過在本次tick中加入一段耗時較長的代碼來來保證本次tick耗時必須超過1ms來檢測:

測試顯示:不論運行多少次,得出的結果都一樣,都是如下:

由此可知,setTimeout是優先于setImmediate被處理的。

總結


要想真正理解很多why的問題,光看大量的案例和看文字解釋其實還是很難理解的,死記硬背也比較難記住。最好的方法還是通過閱讀底層代碼實現,并思考為什么這樣設計,應該就會好很多。這些代碼分析并不完整,我個人的理解也不是非常深入,很多地方地方可能都沒有講清楚。以后應該還會有更多的文章出來進行分析。

通過上面的分析,我也簡單給出幾個結論:

優先級順序:process.nextTick > setTimeout/setInterval > setImmediate

setTimeout需要使用紅黑樹,且after設置為0,其實會被node強制轉換為1,存在性能上的問題,建議替換為setImmediate

process.nextTick有一些比較難懂的問題和隱患,從0.8版本開始加入setImmediate,使用時,建議使用setImmediate

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容