[NodeJs系列]Q&A之理解NodeJs中的Event Loop、Timers以及process.nextTick()

在上一篇文章理解NodeJs中的Event Loop、Timers以及process.nextTick中筆者提了幾個問題,如今針對這些問題給出個人理解,若有錯漏煩請指正。html

若是你對NodeJs系列感興趣,歡迎關注微信公衆號:前端神盾局或 github NodeJs系列文章前端

poll階段何時會被阻塞?

在上一篇文章中提到在poll階段會「接收新的I/O事件而且在適當時node會阻塞在這裏」,那什麼狀況下會阻塞呢?阻塞多久呢?node

對於這個問題,咱們必須深刻到libuv的源碼,看看poll階段是怎麼實現的:c++

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的值?git

再回到源碼中,timeout的初始值爲0,也就意味着poll階段以後會直接轉入check階段而不會發生阻塞。可是當(mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT這些條件成立時,timeout就由uv_backend_timeout的返回值決定。github

這裏須要插播一下關於mode值的問題,根據官方文檔 mode一共有三種狀況:c#

  • 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);
}

這是一個多步條件判斷函數,咱們一個個分析:函數

  1. 若是event loop已(或正在)結束(調用了uv_stop()stop_flag != 0),timeout爲0
  2. 若是沒有異步任務須要處理,timeout爲0
  3. 若是還有未處理的idle_handlespending_queuetimeout爲0(對於idle_handlespending_queue分別表明什麼,筆者尚未概念,若是後面有相應資料會及時更新)
  4. 若是還有存在未清理的資源,timeout爲0
  5. 若是以上條件都不知足,則使用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階段會進行阻塞:

  1. event loop 並未觸發關閉動做
  2. 還有異步隊列沒有處理
  3. 資源已所有關閉

而阻塞的時間最長不超過給定定時器的最小閥值

爲何在非I/O循環中,setTimeoutsetImmediate的執行順序是不必定的?

上文提到setTimeoutsetImmediate在非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的大小,這裏可能出現兩種狀況:

  1. 因爲第一次loop前的準備耗時超過1ms,當前的loop->time >=1 ,則uv_run_timer生效,timeout先執行
  2. 因爲第一次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');
  });
});

因爲timeoutimmediate的事件註冊是在readFile的回調執行時觸發的,因此必然的,在readFile的回調執行前的每一次event loop進來的uv_run_timer都不會有超時事件觸發
那麼當readFile執行完畢,poll階段收到監聽的fd事件完成後,執行了該回調,此時

  1. timeout事件註冊
  2. immediate事件註冊
  3. 因爲readFile的回調執行完畢,那麼就會從uv_io_poll中出來,此時當即執行uv_run_check,因此immediate事件被執行掉
  4. 最後的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函數中查找,這個過程就稱之爲棧展開。

參考

  1. https://zhuanlan.zhihu.com/p/...
  2. https://cnodejs.org/topic/57d...
  3. http://gngshn.github.io/2017/...
  4. http://docs.libuv.org/en/v1.x...

image

相關文章
相關標籤/搜索