Node.js 程式碼閱讀筆記系列 — process.nextTick() 的實現

Awe發表於2017-03-30

process.nextTick()

process 是一個全域性物件,它提供了當前 Node.js 執行緒的相關資訊和一些控制方法。因為 process 掛載了太多屬性和方法,這篇文章先從 process.nextTick() 開始吧。

setupNextTick

function setupNextTick() {
    // 設定 Promise 模組的排程方法
  const promises = require('internal/process/promises');
  const emitPendingUnhandledRejections = promises.setup(scheduleMicrotasks);

  var nextTickQueue = [];
  // microtask 標記
  var microtasksScheduled = false;

  // 接收 V8  micro task 佇列的執行的物件.
  var _runMicrotasks = {};

  // 這裡 kIndex kLength 是一個約定的 Environment::TickInfo 的 index 和 length 的索引 
  var kIndex = 0;
  var kLength = 1;

  process.nextTick = nextTick;
  // Needs to be accessible from beyond this scope.
  process._tickCallback = _tickCallback;
  process._tickDomainCallback = _tickDomainCallback;

  // 通過 process._setupNextTick 註冊 _tickCallback, 獲取 _runMicrotasks
  // `tickInfo` 也接收了 `process._setupNextTick()` 的返回引數,通過 `tickInfo` 能使 C++ 模組能訪問到 nextTick 佇列的狀態。

  const tickInfo = process._setupNextTick(_tickCallback, _runMicrotasks);

    // 接收驅動 V8's micro task 佇列的方法
  _runMicrotasks = _runMicrotasks.runMicrotasks;

    function tickDone() {
    ...
    }

    function scheduleMicrotasks() {
    ...
    }

    function runMicrotasksCallback() {
    ...
    }

    function _combinedTickCallback() {
    ...
    }

    function _tickCallback() {
    ...
    }

    function _tickDomainCallback() {
    ...
    }

    function nextTick() {
    ...
    }
}複製程式碼

_tickCallback & _tickDomainCallback

這裡兩個大體都是執行一定數量( 最大 1e4 )的數量 callbacks, 前者不需要執行 domain 進入上下文。

  function _tickCallback() {
    var callback, args, tock;

    do {
      while (tickInfo[kIndex] < tickInfo[kLength]) {
        tock = nextTickQueue[tickInfo[kIndex]++];
        callback = tock.callback;
        args = tock.args;
        _combinedTickCallback(args, callback);
        if (kMaxCallbacksPerLoop < tickInfo[kIndex])
          tickDone();
      }
      tickDone();
      // V8 promise microtasks
      _runMicrotasks();
      emitPendingUnhandledRejections();
    } while (tickInfo[kLength] !== 0);
  }複製程式碼

_combinedTickCallback

這裡的引數處理還是體現了 Nodejs 中貫穿的效能追求以及 80/20 的理念。

  function _combinedTickCallback(args, callback) {
    if (args === undefined) {
      callback();
    } else {
      switch (args.length) {
        case 1:
          callback(args[0]);
          break;
        case 2:
          callback(args[0], args[1]);
          break;
        case 3:
          callback(args[0], args[1], args[2]);
          break;
        default:
          callback.apply(null, args);
      }
    }
  }複製程式碼

tickDone

執行正常的清理操作,刪除剛執行完的 callback 或者 清空佇列。

  function tickDone() {
    if (tickInfo[kLength] !== 0) {
      if (tickInfo[kLength] <= tickInfo[kIndex]) {
        nextTickQueue = [];
        tickInfo[kLength] = 0;
      } else {
           // 推出佇列的首個元素
        nextTickQueue.splice(0, tickInfo[kIndex]);
        tickInfo[kLength] = nextTickQueue.length;
      }
    }
    tickInfo[kIndex] = 0;
  }複製程式碼

scheduleMicrotasks

再回頭看一下 setupNextTick 中的 Promise setup 那段 promises.setup(scheduleMicrotasks)。下面我們來看看 scheduleMicrotasks.

function setupNextTick() {
  const promises = require('internal/process/promises');
  const emitPendingUnhandledRejections = promises.setup(scheduleMicrotasks);
  var microtasksScheduled = false;
  ...
}複製程式碼

先判斷 microtasksScheduled ,如果為 false 就會執行到給 nextTickQueue 新增一個新的節點,callbackrunMicrotasksCallback。接著 tickInfo[kLength] 增加 1 並將 microtasksScheduled 設定為 true , 確保在未執行 microtask 之前不會重複執行。

  function scheduleMicrotasks() {
    if (microtasksScheduled)
      return;

    nextTickQueue.push({
      callback: runMicrotasksCallback,
      domain: null
    });

    tickInfo[kLength]++;
    microtasksScheduled = true;
  }複製程式碼

runMicrotasksCallback

可以看到這裡與之前對應的, 這裡首先執行 microtasksScheduled = false, 接著呼叫 _runMicrotasks。在 nextTickQueue 以及 Promise 還有 Listeners 時繼續呼叫 scheduleMicrotasks 來向 nextTickQueue 新增 callback。


  function runMicrotasksCallback() {
    microtasksScheduled = false;
    _runMicrotasks();

    if (tickInfo[kIndex] < tickInfo[kLength] ||
        emitPendingUnhandledRejections())
      scheduleMicrotasks();
  }複製程式碼

SetupNextTick

通過上面的 JS 部分我們瞭解到,process.nextTick, Microtasks 以及 Promise 的 callback 都是通過一個佇列 nextTickQueue 排程, 而這一切都是從
_tickCallback_tickDomainCallback )開始的。

// src/node.cc
void SetupNextTick(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);

  CHECK(args[0]->IsFunction());
  CHECK(args[1]->IsObject());

  // 將之前的 `_tickCallback` 設定到環境變數中 tick_callback_function
  env->set_tick_callback_function(args[0].As<Function>());

    // 將傳過來的 _runMicrotasks ({}) 物件新增 runMicrotasks 方法
  env->SetMethod(args[1].As<Object>(), "runMicrotasks", RunMicrotasks);

  // Do a little housekeeping.
  // 刪除當前執行環境的執行緒上的 _setupNextTick
  env->process_object()->Delete(
      env->context(),
      FIXED_ONE_BYTE_STRING(args.GetIsolate(), "_setupNextTick")).FromJust();


  // 返回 tick_info 用於和 processNextTick js部分能同步狀態

  uint32_t* const fields = env->tick_info()->fields();
  uint32_t const fields_count = env->tick_info()->fields_count();

  Local<ArrayBuffer> array_buffer =
      ArrayBuffer::New(env->isolate(), fields, sizeof(*fields) * fields_count);

    //返回一個陣列 [0, 0] 
  // 和 lib/internal/process/next_tick.js 中
  // kIndex, kLength 對應

  args.GetReturnValue().Set(Uint32Array::New(array_buffer, 0, fields_count));

}複製程式碼

RunMicrotasks() 是 v8 暴露的一個 API 方法
cs.chromium.org/chromium/sr…

上面設定 tick_callback_function,那麼這個 process.nextTick() 是什麼時候被呼叫?

AsyncWrap

// src/async-wrap.cc

Local<Value> AsyncWrap::MakeCallback(const Local<Function> cb,
                                     int argc,
                                     Local<Value>* argv) {

  ...

  Local<Value> ret = cb->Call(context, argc, argv);

  ...

  Environment::TickInfo* tick_info = env()->tick_info();

    // 如果 nextTick 佇列為空時執行 RunMicrotasks
  if (tick_info->length() == 0) {
    env()->isolate()->RunMicrotasks();
  }

  Local<Object> process = env()->process_object();

  if (tick_info->length() == 0) {
    tick_info->set_index(0);
    return ret;
  }

  // 直接執行 _tickCallback
  if (env()->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) {
    return Local<Value>();
  }

  return ret;
}複製程式碼

HandleWrap

// - MakeCallback may only be made directly off the event loop.
//   That is there can be no JavaScript stack frames underneath it.

// MakeCallback 會直接在 event loop 中執行。

class HandleWrap : public AsyncWrap {
 public:
  ...
  uv_handle_t* const handle_;
};複製程式碼

比如下面的 UDPWrap 是 Nodejs 的 udp 協議 (User Datagram Protocol)的封裝層,它繼承自 HandleWrap

// src/udp_wrap.cc
class UDPWrap: public HandleWrap {
 public:
  ...
 private:
  ...
  uv_udp_t handle_;
};複製程式碼

AsyncWrap 是 Nodejs 中大多數 IO 封裝層都是基於 HandleWrapHandleWrap 繼承自 AsyncWrap , 所以 process.nextTick 和 microtask 基本是在 uv__io_poll 階段呼叫, 為什麼說是主要,因為有兩個其他情況,繼續往下看。

node 初始化

node 初始化執行的時候會呼叫 process._tickCallback()

// lib/module.js

// bootstrap main module.
Module.runMain = function() {
  // Load the main module--the command line argument.
  Module._load(process.argv[1], null, true);
  // Handle any nextTicks added in the first tick of the program
  process._tickCallback();
};複製程式碼

捕獲異常

如果應用中丟擲異常,未被捕獲的話退出執行緒,有捕獲的話,應用不會崩潰退出,而是呼叫 setImmediate 執行 process._tickCallback(), 也就是說 process.nextTick 也可能在 Check 階段被呼叫。

function setupProcessFatal() {

    process._fatalException = function(er) {
      var caught;

      if (process.domain && process.domain._errorHandler)
        caught = process.domain._errorHandler(er) || caught;

      if (!caught)
        caught = process.emit('uncaughtException', er);
       // 如果沒有函式處理這個異常,C++ 結束
      if (!caught) {
        try {
          if (!process._exiting) {
            process._exiting = true;
            process.emit('exit', 1);
          }
        } catch (er) {
          // nothing to be done about it at this point.
        }
      } else {
        // 如果捕獲了這個異常,在 `setImmediate` 中呼叫 `_tickCallback()` 繼續處理 nextTick 佇列
        NativeModule.require('timers').setImmediate(process._tickCallback);
      }

      return caught;
    };
  }複製程式碼

scheduleMicrotasks?

function scheduleMicrotasks() {
    if (microtasksScheduled)
      return;

    nextTickQueue.push({
      callback: runMicrotasksCallback,
      domain: null
    });

    tickInfo[kLength]++;
    microtasksScheduled = true;
}複製程式碼

MakeCallback

//  src/node.cc
Local<Value> MakeCallback() {
  ...
   // V8 RunMicrotasks 
  if (tick_info->length() == 0) {
    env->isolate()->RunMicrotasks();
  }
  ...

  return ret;
}複製程式碼

Promise

V8 中 microtask 預設是自動執行的。因為 Promise 處理的非同步場景和絕大多數 Nodejs 中非同步IO 是緊密相關的,所以在 Nodejs 中預設關閉了自動執行而通過 Nodejs 自行觸發 RunMicrotasks()。結合上面的程式碼也可以基本得出結論 Nodejs 中 Promiseprocess.nextTick() 回撥的執行階段是比較相似的。

inline int Start(..) {
...
  isolate->SetAutorunMicrotasks(false);
...複製程式碼

總結

process.nextTick() 一般是在 poll 階段被執行,也有可能在 check 階段執行。Promise 所處的 Microtasks 是通過呼叫 V8 暴露的 RunMicrotasks() 方法執行,RunMicrotasks() 會在 process.nextTick() 佇列執行,也會在 node::MakeCallback 中執行。

參考資料

lib/internal/process/next_tick.js

src/node.cc

lib/internal/process/promises.js

/src/handle_wrap.h

chromium/src/v8/src/api.cc

相關文章