nodejs 的event loop源碼解讀

準備努力在cNode中硬凹存在感,立一個不打臉的flag
註明原文出處(社區豪華版):不要混淆nodejs和瀏覽器中的event loop
很多時候nodejs和瀏覽器端的event loop會被混爲一談,但是需要明確的點是:
nodejs和瀏覽器的event loop是有着明確區分的事物,不能夠混爲一談

下面的這個點很重要。我自己也在努力踐行中,和小夥伴們一起加油了。
討論一些js異步代碼的執行順序的時候,要基於node的源碼而不是自己的臆想。

nodejs的event是基於libuv,而瀏覽器的event則是在html5規範中明確定義HTML5.2文檔鏈接

libuv已經對event loop做出了實現,而htrml5只是定義了瀏覽器的event loop模型,具體實現交給了瀏覽器廠商

nodejs中的event loop

nodejs的官方文檔對於Nodejs的event loop有一個比較完整的描述。
libuv官方文檔做了更加細緻的描述
nodejs的發展很快,跟着大神的腳步學習源碼,下面的討論均基於nodejs9.5.0

#event loop的六個階段

nodejs的event loop分爲6個階段,每個階段的作用如下:
(process.nextTick()在六個階段結束的時候都會執行,文章後半部分會詳細分析process.nextTick()的回調怎麼引進event loop,僅僅從uv_run()是找不到process.nextTick()是如何牽涉進來)

1.timers:執行setTimeout()和setInterval()中到期的callback。
2.I/O callbacks:上一輪中有少數的I/O callback會被延遲到這一輪的這一階段執行。
3.idle,prepare:僅在內部使用
4.poll:最爲重要的一個階段,執行I/O callback,在適當的條件下會阻塞在這個階段
5.check:執行setImmediate的callback
6.close callbacks:執行close事件的callback,例如socket.on(“close”,func)

這裏寫圖片描述

event loop的每次循環都需要依次經過上述的階段。每個階段都有自己的callback隊列,每當進入某個階段,都會從所屬的隊列中取出callback來執行,當隊列爲空或者被執行的callback的數量達到系統的最大數量時,進入下一個階段。這六個階段都執行完畢稱爲一輪循環。

event loop的核心代碼在deps/uv/src/unix/core.c

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int ran_pending;

 /* 從uv__loop_alive中我們知道event loop繼續的條件是以下三者之一:
  1,有活躍的handles(libuv定義handle就是一些long-lived objects,例如tcp server這樣)
  2,有活躍的request
  3,loop中的closing_handles
*/
  r = uv__loop_alive(loop);
  if (!r)
    uv__update_time(loop);

  while (r != 0 && loop->stop_flag == 0) {
  //更新時間變量,這個變量在uv__run_timers中會用到
    uv__update_time(loop);
  //timers階段
    uv__run_timers(loop);
  //I/O callback階段,ran_pending指示隊列是否爲空
    ran_pending = uv__run_pending(loop);
  //idle階段
    uv__run_idle(loop);
  //prepare階段
    uv__run_prepare(loop);

    timeout = 0;

    /**
    設置poll階段的超時時間,以下幾種情況下超時會被設爲0,這意味着此時poll階段不會被阻塞,在下面的poll階段我們還會詳細討論這個
    1,stop_flag不爲0
    2,沒有活躍的handles和request
    3,idle、I/O callback、close階段的handle隊列不爲空
    否則,設爲timer階段的callback隊列中,距離當前時間最近的那個
    **/    
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop);

   //poll階段
    uv__io_poll(loop, timeout);
   //check階段
    uv__run_check(loop);
   //close階段
    uv__run_closing_handles(loop);
   //如果mode == UV_RUN_ONCE(意味着流程繼續向前)時,在所有階段結束後還會檢查一次timers,這個的邏輯的原因不太明確

    if (mode == UV_RUN_ONCE) {
      uv__update_time(loop);
      uv__run_timers(loop);
    }

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

  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}

前輩對重要部分加上了註釋,從上述代碼可以看到event loop的六個階段是依次執行的。值得注意的是,在UV_RUN_ONCE模式下,timers階段在當前循環結束前還會得到一次的執行機會。
下面來分別看一下各個階段:

timers階段:

timer階段的代碼在deps/uv/src/unix/timer.c的uv__run_timers()中

void uv__run_timers(uv_loop_t* loop) {
  struct heap_node* heap_node;
  uv_timer_t* handle;

  for (;;) {
    heap_node = heap_min((struct heap*) &loop->timer_heap);//取出timer堆上超時時間最小的元素
    if (heap_node == NULL)
      break;
    //根據上面的元素,計算出handle的地址,head_node結構體和container_of的結合非常巧妙,值得學習
    handle = container_of(heap_node, uv_timer_t, heap_node);
    if (handle->timeout > loop->time)//如果最小的超時時間比循環運行的時間還要大,則表示沒有到期的callback需要執行,此時退出timer階段
      break;

    uv_timer_stop(handle);//將這個handle移除
    uv_timer_again(handle);//如果handle是repeat類型的,重新插入堆裏
    handle->timer_cb(handle);//執行handle上的callback
  }
}

從上面的邏輯可知,在timer階段其實使用一個最小堆而不是隊列來保存所有元素(其實也可以理解,因爲timeout的callback是按照超時時間的順序來調用的,並不是先進先出的隊列邏輯),然後循環取出所有到期的callback執行。

I/O callbacks階段

I/O callbacks階段的代碼在deps/uv/src/unix/core.c的int uv__run_pending()中:

static int uv__run_pending(uv_loop_t* loop) {
  QUEUE* q;
  QUEUE pq;
  uv__io_t* w;

  if (QUEUE_EMPTY(&loop->pending_queue))//如果隊列爲空則退出
    return 0;

  QUEUE_MOVE(&loop->pending_queue, &pq);//移動該隊列

  while (!QUEUE_EMPTY(&pq)) {
    q = QUEUE_HEAD(&pq);//取出隊列的頭結點
    QUEUE_REMOVE(q);//將其移出隊列
    QUEUE_INIT(q);//不再引用原來隊列的元素
    w = QUEUE_DATA(q, uv__io_t, pending_queue);
    w->cb(loop, w, POLLOUT);//執行callbak直到隊列爲空
  }
  return 1;
}
idle和prepare階段

uv__run_idle()、uv__run_prepare()、uv__run_check()定義在文件deps/uv/src/unix/loop-watcher.c中,它們的邏輯非常相似,其中的實現利用了大量的宏(說實在我個人非常煩宏,它的可讀性真的很差,爲了那點點的性能而使用宏真是值得商榷)。

 void uv__run_##name(uv_loop_t* loop) {                                      
    uv_##name##_t* h;                                                         
    QUEUE queue;                                                              
    QUEUE* q;                                                                 
    QUEUE_MOVE(&loop->name##_handles, &queue);//用新的頭節點取代舊的頭節點,相當於將原隊列移動到新隊列                                
    while (!QUEUE_EMPTY(&queue)) {//當新隊列不爲空                                            
      q = QUEUE_HEAD(&queue);//取出新隊列首元素                                                 
      h = QUEUE_DATA(q, uv_##name##_t, queue);//獲取首元素中指向的handle                                
      QUEUE_REMOVE(q);//將這個元素移出新隊列                                                        
      QUEUE_INSERT_TAIL(&loop->name##_handles, q);//然後再插入舊隊列尾部                            
      h->name##_cb(h);//執行對應的callback                                                        
    }                                                                        
  } 
poll階段
poll階段的代碼+註釋高達200行不好逐行分析,我們挑選部分重要代碼

void uv__io_poll(uv_loop_t* loop, int timeout) {
    //...
    //處理觀察者隊列
    while (!QUEUE_EMPTY(&loop->watcher_queue)) {
        //...
    if (w->events == 0)
      op = UV__EPOLL_CTL_ADD;//新增監聽這個事件
    else
      op = UV__EPOLL_CTL_MOD;//修改這個事件
    }
    //...
    //阻塞直到監聽的事件來臨,前面已經算好timeout以防uv_loop一直阻塞下去
    if (no_epoll_wait != 0 || (sigmask != 0 && no_epoll_pwait == 0)) {
      nfds = uv__epoll_pwait(loop->backend_fd,
                events,
                ARRAY_SIZE(events),
                timeout,
                sigmask);
      if (nfds == -1 && errno == ENOSYS)
        no_epoll_pwait = 1;
    } else {
      nfds = uv__epoll_wait(loop->backend_fd,
               events,
               ARRAY_SIZE(events),
               timeout);
      if (nfds == -1 && errno == ENOSYS)
        no_epoll_wait = 1;
    }
    //...
    for (i = 0; i < nfds; i++) {
        if (w == &loop->signal_io_watcher)
          have_signals = 1;
        else
          w->cb(loop, w, pe->events);//執行callback
    }
    //...
}

可見poll階段的任務就是阻塞等待監聽的事件來臨,然後執行對應的callback,其中阻塞是帶有超時時間的,以下幾種情況都會使得超時時間爲0

uv_run處於UV_RUN_NOWAIT模式下
uv_stop()被調用
沒有活躍的handles和request
有活躍的idle handles
有等待關閉的handles
如果上述都不符合,則超時時間爲距離現在最近的timer;如果沒有timer則poll階段會一直阻塞下去

check階段

見上面的 idle和prepare階段

close階段
static void uv__run_closing_handles(uv_loop_t* loop) {
  uv_handle_t* p;
  uv_handle_t* q;

  p = loop->closing_handles;
  loop->closing_handles = NULL;

  while (p) {
    q = p->next_closing;
    uv__finish_close(p);
    p = q;
  }
}

這段代碼就是循環關閉所有的closing handles,其中的callback調用在uv__finish_close()中

process.nextTick在哪裏

文檔中提到process.nextTick()不屬於上面的任何一個phase,它在每個phase結束的時候都會運行。但是我們看到uv_run()中只是依次運行了6個phase的函數,並沒有process.nextTick()影子,那它是怎麼被驅動起來的呢?

這個問題要從兩個c++和js的源碼層面來說明。

process.nextTick在js層面上的體現

process.nextTick的實現在next_tick.js中

 function nextTick(callback) {
    if (typeof callback !== 'function')
      throw new errors.TypeError('ERR_INVALID_CALLBACK');

    if (process._exiting)
      return;

    var args;
    switch (arguments.length) {
      case 1: break;
      case 2: args = [arguments[1]]; break;
      case 3: args = [arguments[1], arguments[2]]; break;
      case 4: args = [arguments[1], arguments[2], arguments[3]]; break;
      default:
        args = new Array(arguments.length - 1);
        for (var i = 1; i < arguments.length; i++)
          args[i - 1] = arguments[i];
    }

    push(new TickObject(callback, args, getDefaultTriggerAsyncId()));//將callback封裝爲一個對象放入隊列中
  }

它並沒有什麼魔法,也沒有調用C++提供的函數,只是簡單地將所有回調封裝爲對象並放入隊列。而callback的執行是在函數_tickCallback()

function _tickCallback() {
    let tock;
    do {
      while (tock = shift()) {
        const asyncId = tock[async_id_symbol];
        emitBefore(asyncId, tock[trigger_async_id_symbol]);
        if (destroyHooksExist())
          emitDestroy(asyncId);

        const callback = tock.callback;
        if (tock.args === undefined)
          callback();//執行調用process.nextTick()時放置進來的callback
        else
          Reflect.apply(callback, undefined, tock.args);//執行調用process.nextTick()時放置進來的callback

        emitAfter(asyncId);
      }
      runMicrotasks();//microtasks將會在此時執行,例如Promise
    } while (head.top !== head.bottom || emitPromiseRejectionWarnings());
    tickInfo[kHasPromiseRejections] = 0;
  }

可以看到_tickCallback()會循環執行隊列中所有callback,值得注意的是microtasks的執行時機, 因此_tickCallback()的執行就意味着process.nextTick()的回調的執行。我們繼續搜索一下發現_tickCallback()在好幾個地方都有被調用,但是我們只關注跟event loop相關的。
在next_tick.js中發現

 const [
    tickInfo,
    runMicrotasks
  ] = process._setupNextTick(_tickCallback);

查找了一下發現在node.cc中有


env->SetMethod(process, "_setupNextTick", SetupNextTick);//暴露_setupNextTick給js
_setupNextTick()是node.cc那邊暴露出來的方法,因此猜測這就是連接event loop的橋樑。
C++中執行process.nextTick的回調

在node.cc中找出SetNextTick()函數,有這樣的代碼片段

void SetupNextTick(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);

  CHECK(args[0]->IsFunction());
  //把js中提供的回調函數(即_tickCallback)保存起來,以供調用
  env->set_tick_callback_function(args[0].As<Function>());
  ...
}

_tickCallback被放置到env裏面去了,那它何時被調用?也是在node.cc中我們發現

void InternalCallbackScope::Close() {
  if (!tick_info->has_scheduled()) {
    env_->isolate()->RunMicrotasks();
  }
  //...
  //終於調用在SetupNextTick()中放置進來的函數了
  if (env_->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) {
    env_->tick_info()->set_has_thrown(true);
    failed_ = true;
  }
}

可知InternalCallbackScope::Close()會調用它,而InternalCallbackScope::Close()則在文件node.cc的InternalMakeCallback()中被調用

MaybeLocal<Value> InternalMakeCallback(Environment* env,
                                       Local<Object> recv,
                                       const Local<Function> callback,
                                       int argc,
                                       Local<Value> argv[],
                                       async_context asyncContext) {
  CHECK(!recv.IsEmpty());
  InternalCallbackScope scope(env, recv, asyncContext);
  //...
  scope.Close();//Close會調用_tickCall
  //...
}

而InternalMakeCallback()則是在async_wrap.cc的AsyncWrap::MakeCallback()中被調用

MaybeLocal<Value> AsyncWrap::MakeCallback(const Local<Function> cb,
                                          int argc,
                                          Local<Value>* argv) {
  //cb就是在event loop的6個phase中執行的回調函數
  MaybeLocal<Value> ret = InternalMakeCallback(env(), object(), cb, argc, argv, context);
}

AsyncWrap類是異步操作的封裝,它是一個頂級的類,TimerWrap、TcpWrap等封裝異步的類都繼承了它,這意味着這些類封裝異步操作的時候都會調用MakeCallback()。至此真相大白了,uv_run()中的回調都是經過AsyncWrap::MakeCallback()包裝過的,因此回調執行完畢之後都會執行process.nextTick()的回調了,與文檔的描述是相符合的。整理一下_tickCallback()的轉移並最終被調用的流程


在js層面

_tickCallback()//js中執行process.nextTick()的回調函數
        ↓
process._setupNextTick(_tickCallback)       //c++和js的橋樑,將回調交給C++執行
此時_tickCallback()被轉移到在C++層面,它首先被存儲到env中

env->set_tick_callback_function()//將_tickCallback存儲到env中
        ↓       
env->SetMethod(process, "_setupNextTick", SetupNextTick);//調用上者,js中process._setupNextTick的真身
被存儲到env的_tickCallback()被調用流程如下:

env_->tick_callback_function()//取出_tickCallback執行
        ↓
InternalCallbackScope::Close()//調用前者
        ↓  
InternalMakeCallback()//調用前者   
        ↓  
AsyncWrap::MakeCallback()//調用前者   
        ↓  
被多個封裝異步操作的類繼承並調用
        ↓
被uv_run()執行,從而實現在每個phase之後調用process.nextTick提供的回調   
發佈了144 篇原創文章 · 獲贊 67 · 訪問量 35萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章