iT邦幫忙

2021 iThome 鐵人賽

DAY 23
0
自我挑戰組

從C到JS的同步非同步探索系列 第 23

[Day 23] Node Event loop 2

前言

今天繼續看看 event loop 的核心循環, uv_run() , 可以查看以下網址

https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/deps/uv/src/win/core.c

主要循環

int uv_run(uv_loop_t *loop, uv_run_mode mode) {
  DWORD 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_process_reqs(loop);
    uv_idle_invoke(loop);
    uv_prepare_invoke(loop);

    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop);

    if (pGetQueuedCompletionStatusEx)
      uv__poll(loop, timeout);
    else
      uv__poll_wine(loop, timeout);

    /* Run one final update on the provider_idle_time in case uv__poll*
     * returned because the timeout expired, but no events were received. This
     * call will be ignored if the provider_entry_time was either never set (if
     * the timeout == 0) or was already updated b/c an event was received.
     */
    uv__metrics_update_idle_time(loop);

    uv_check_invoke(loop);
    uv_process_endgames(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__run_timers(loop);
    }

    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

  /* The if statement lets the compiler compile it to a conditional store.
   * Avoids dirtying a cache line.
   */
  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}

原則上這個循環就是不斷的在處理任務, 可以稱得上是 node.js 背後默默做事的工具人了。

今天我們先看

uv_update_time(loop);
uv__run_timers(loop);

這兩句

uv_update_time

void uv_update_time(uv_loop_t* loop) {
  uint64_t new_time = uv__hrtime(1000);
  assert(new_time >= loop->time);
  loop->time = new_time;
}

主要是利用 uv__hrtime 取得新的時間

static void uv__hrtime_init_once(void) {
  if (KERN_SUCCESS != mach_timebase_info(&timebase))
    abort();

  time_func = (uint64_t (*)(void)) dlsym(RTLD_DEFAULT, "mach_continuous_time");
  if (time_func == NULL)
    time_func = mach_absolute_time;
}

uint64_t uv__hrtime(uv_clocktype_t type) {
  uv_once(&once, uv__hrtime_init_once);
  return time_func() * timebase.numer / timebase.denom;
}

uv__hrtime 主要就是調用一些 OS API 取得當下時間

之所以不每次要時間都和 OS 要, 是為了避免 , system call 會阻塞運作, 所以盡量都維持在 user mode

uv__run_timers

重點環節, 提供註釋

void uv__run_timers(uv_loop_t* loop) {
	// 取得 heap 首節點
  struct heap_node* heap_node;
  uv_timer_t* handle;

  for (;;) {
		// 取得 heap 中最小節點
    heap_node = heap_min(timer_heap(loop));
		// heap 為空則斷開
    if (heap_node == NULL)
      break;
		// 當 最小節點也大於當前時間, 就退出
    handle = container_of(heap_node, uv_timer_t, heap_node);
    if (handle->timeout > loop->time)
      break;
		// 刪除節點
    uv_timer_stop(handle);
		// 如果是重複任務, 就再插回 heap
    uv_timer_again(handle);
		// 執行節點任務
    handle->timer_cb(handle);
  }
}

這句方法主要是用來處理需要定期觸發的任務, 可以看到 node 維護了一個 heap ( 最小樹 ) , 當有以時間為觸發基準的任務被註冊後, 就包成節點放入 heap , 節點中包含事件觸發時間及其回調函數。

因為最小樹的頂端永遠是最小節點 (在這裡可以視為觸發時間最早者) , 所以可以用最快的速度鎖定要處理的任務。 以下簡述流程。

  1. JS 層註冊事件要求 5 秒後觸發 callback
  2. heap 被放入節點, 節點值是 5 秒後的時間, 且因為其為 heap 中的最小數, 所以在樹頂
  3. 6 秒後 uv_run 運作到 uv__run_timers
  4. 取出樹頂, 發現觸發時間晚於當前時間, 表示該觸發了
  5. 刪除節點
  6. 執行節點所包含的 callback

明天進度

明天我們來看看 , uv__io_poll(loop, timeout) ,中間會先跳過一些階段, 希望藉此可以在看其他階段前有更宏觀的視野。

明天見 !


上一篇
[Day 22] Node Event loop 1
下一篇
[Day 24] Node Event loop 3
系列文
從C到JS的同步非同步探索30

尚未有邦友留言

立即登入留言