在上一篇文章理解NodeJs中的Event Loop、Timers以及process.nextTick中筆者提了幾個問題,現在針對這些問題給出我的理解,如有錯漏煩請指正。
如果你對NodeJs系列感興趣,歡迎關注前端神盾局或筆者微信(w979436427)交流討論node學習心得
poll階段什麼時候會被阻塞?
在上一篇文章中提到在poll階段會“接收新的I/O事件並且在適當時node會阻塞在這裡”,那什麼情況下會阻塞呢?阻塞多久呢?
對於這個問題,我們必須深入到libuv的原始碼,看看poll階段是怎麼實現的:
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
r = uv__loop_alive(loop);
if (!r)
uv__update_time(loop);
while (r != 0 && loop->stop_flag == 0) {
uv__update_time(loop);
uv__run_timers(loop);
ran_pending = uv__run_pending(loop);
uv__run_idle(loop);
uv__run_prepare(loop);
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
// 這是poll階段
uv__io_poll(loop, timeout);
uv__run_check(loop);
uv__run_closing_handles(loop);
if (mode == UV_RUN_ONCE) {
/* UV_RUN_ONCE implies forward progress: at least one callback must have
* been invoked when it returns. uv__io_poll() can return without doing
* I/O (meaning: no callbacks) when its timeout expires - which means we
* have pending timers that satisfy the forward progress constraint.
*
* UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from
* the check.
*/
uv__update_time(loop);
uv__run_timers(loop);
}
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
/* The if statement lets gcc compile it to a conditional store. Avoids
* dirtying a cache line.
*/
if (loop->stop_flag != 0)
loop->stop_flag = 0;
return r;
}
複製程式碼
從原始碼我們可以看到uv__io_poll
傳入了timeout
作為引數,而這個timeout
就決定了poll階段阻塞的時長,明白這一點我們就可以把問題轉化成:是什麼決定的timeout
的值?
再回到原始碼中,timeout
的初始值為0,也就意味著poll階段之後會直接轉入check階段而不會發生阻塞。但是當(mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT
這些條件成立時,timeout
就由uv_backend_timeout
的返回值決定。
這裡需要插播一下關於mode
值的問題,根據官方文件 mode
一共有三種情況:
UV_RUN_DEFAULT
UV_RUN_ONCE
UV_RUN_NOWAIT
這裡我們只關心UV_RUN_DEFAULT
,因為Node event loop使用的是這種模式.
OK~回到問題,我們再看一下uv_backend_timeout
會返回什麼?
int uv_backend_timeout(const uv_loop_t* loop) {
if (loop->stop_flag != 0)
return 0;
if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
return 0;
if (!QUEUE_EMPTY(&loop->idle_handles))
return 0;
if (!QUEUE_EMPTY(&loop->pending_queue))
return 0;
if (loop->closing_handles)
return 0;
return uv__next_timeout(loop);
}
複製程式碼
這是一個多步條件判斷函式,我們一個個分析:
- 如果event loop已(或正在)結束(呼叫了
uv_stop()
,stop_flag != 0
),timeout
為0 - 如果沒有非同步任務需要處理,
timeout
為0 - 如果還有未處理的
idle_handles
和pending_queue
,timeout
為0(對於idle_handles
和pending_queue
分別代表什麼,筆者還沒有概念,如果後面有相應資料會及時更新) - 如果還有存在未清理的資源,
timeout
為0 - 如果以上條件都不滿足,則使用
uv__next_timeout
處理
int uv__next_timeout(const uv_loop_t* loop) {
const struct heap_node* heap_node;
const uv_timer_t* handle;
uint64_t diff;
heap_node = heap_min((const struct heap*) &loop->timer_heap);
if (heap_node == NULL)
return -1; /* block indefinitely */
handle = container_of(heap_node, uv_timer_t, heap_node);
if (handle->timeout <= loop->time)
return 0;
// 這句程式碼給出了關鍵性的指導
// 對比當前loop的時間戳
diff = handle->timeout - loop->time;
//不能大於最大的INT_MAX
if (diff > INT_MAX)
diff = INT_MAX;
return diff;
}
複製程式碼
總結一下,event loop 滿足以下條件時,poll階段會進行阻塞:
- event loop 並未觸發關閉動作
- 還有非同步佇列沒有處理
- 資源已全部關閉
而阻塞的時間最長不超過給定定時器的最小閥值
為什麼在非I/O迴圈中,setTimeout
和setImmediate
的執行順序是不一定的?
上文提到setTimeout
和setImmediate
在非I/O迴圈中,執行順序是不一定的,比如:
setTimeout(function timeout() {
console.log('timeout');
}, 0);
setImmediate(function immediate() {
console.log('immediate');
});
複製程式碼
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
複製程式碼
相同程式碼,兩次執行結果卻是相反的,這是為什麼呢?
在node中,setTimeout(cb, 0) === setTimeout(cb, 1)
在event loop的第一個階段(timers階段),node都會從一堆定時器中取出一個最小閥值的定時器來與loop->time
進行比較,如果閥值小於等於loop->time
表示定時器已超時,相應的回撥便會執行(隨後會檢查下一個定時器),如果沒有則會進入下一個階段。
所以setTimeout
是否在第一階段執行取決於loop->time
的大小,這裡可能出現兩種情況:
-
由於第一次loop前的準備耗時超過1ms,當前的
loop->time >=1
,則uv_run_timer
生效,timeout
先執行 -
由於第一次loop前的準備耗時小於1ms,當前的
loop->time < 1
,則本次loop中的第一次uv_run_timer
不生效,那麼io_poll
後先執行uv_run_check
,即immediate
先執行,然後等close cb
執行完後,繼續執行uv_run_timer
這就是為什麼同一段程式碼,執行結果隨機的緣故。那為什麼說在I/O回撥中,一定是先immediate
執行呢,其實也很容易理解,考慮以下場景:
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
複製程式碼
由於timeout
和immediate
的事件註冊是在readFile
的回撥執行時觸發的,所以必然的,在readFile
的回撥執行前的每一次event loop進來的uv_run_timer
都不會有超時事件觸發
那麼當readFile
執行完畢,poll階段收到監聽的fd事件完成後,執行了該回撥,此時
timeout
事件註冊immediate
事件註冊- 由於
readFile
的回撥執行完畢,那麼就會從uv_io_poll
中出來,此時立即執行uv_run_check
,所以immediate
事件被執行掉 - 最後的
uv_run_timer
檢查timeout
事件,執行timeout
事件
所以你會發現,在I/O回撥中註冊的兩者,永遠都是immediately
先執行
JS呼叫棧被展開是什麼意思?
棧展開主要是指在丟擲異常後逐層匹配catch語句的過程,舉個例子:
function a(){
b();
}
function b(){
c();
}
function c(){
throw new Error('from function c');
}
a();
複製程式碼
這個例子中,函式c
丟擲異常,這是首先會在c
函式本身檢查是否存在try相關的catch語句,如果沒有就退出當前函式,並且釋放當前函式的記憶體並銷燬區域性物件,繼續到b
函式中查詢,這個過程就稱之為棧展開。