標籤:

Process.nextTick 和 setImmediate 的區別?

求解答


A();
B();
C();

------------------------------------------------------------------------

A();
process.nextTick(B);
C();

------------------------------------------------------------------------

A();
setImmediate(B);
C();


寫在前面——偶不會node,是純來湊熱鬧的。

要說有啥區別么,偶也不知道,正如前面說的。

為了跟 @徐飛 大叔 和 @pw 哥 沒事兒逗悶子玩,偶只好去扒代碼看看實現,然後就去 github 跟嘍。

恩,涉及這倆貨的主要有這幾個文件(基於 node 0.10.33):

1、http://node.cc 關鍵都在裡頭了

2、node.js 姑且叫做業務包裝吧,基本在裡頭了

3、lib/timers.js setImmediate 的業務包裝在裡頭了

然後發現吧,setImmediate 和 nextTick 其實都是使用的 Process 實現,都是與 loop 有關。

只是,setImmediate 真的很蛋疼,非要裝作是個 timers 還實現 clearImmediate …… 無語啊。

反正吧不管怎麼樣,它還是用的 process._needImmediateCallback = true;

然後看 http://node.cc 吧。裡頭有:

process-&>SetAccessor(String::New("_needImmediateCallback"),
NeedImmediateCallbackGetter,
NeedImmediateCallbackSetter);

Getter 和 Setter 么就是, process._needImmediateCallback = true 就調用 NeedImmediateCallbackSetter 函數了。裡頭吧:

uv_check_start(check_immediate_watcher, node::CheckImmediate);
// idle handle is needed only to maintain event loop
uv_idle_start(idle_immediate_dummy, node::IdleImmediateDummy);

關鍵的來了……

uv_check_start 先不管是啥,留最後說(關鍵就這玩意),這東西呢說 uv 轉了檢查了以後就執行 CheckImmediate 這玩意。CheckImmediate 里就是把掛的 callback 都跑一遍。

uv_idle_start 這玩意注釋說了就是保持個事件循環啥也沒做,人家注釋也說了么。

nextTick 呢,先看 node.js, 裡頭有一行

process.maxTickDepth = 1000

然後在 _nextTick和 _nextDomainTick 里是有個要求的

if (infoBox[depth] &>= process.maxTickDepth)
maxTickWarn();

function maxTickWarn() {
// XXX Remove all this maxTickDepth stuff in 0.11
var msg = "(node) warning: Recursive process.nextTick detected. " +
"This will break in the next version of node. " +
"Please use setImmediate for recursive deferral.";
if (process.throwDeprecation)
throw new Error(msg);
else if (process.traceDeprecation)
console.trace(msg);
else
console.error(msg);
}

超 1k 深度是會報錯的。

在 _tickDomainCallback 和 _tickCallback 里是 infoBox[depth]++ 的。

好了,

第一個不同出現—— nextTick 有調用深度限制 1000 個,setImmediate 就木有~

在看到 http://node.cc 里。nextTihttp://docs.libuv.org/en/v1.x/check.html#c.uv_check_startck 的"最終"實現 NeedTickCallback:

static Handle& NeedTickCallback(const Arguments args) {
need_tick_cb = true;
uv_idle_start(tick_spinner, Spin);
return Undefined();
}

咦,也有個 uv_idle_start ,好巧呢 &>_&<

那好,回頭查查這倆東西是啥吧。

新虧有 libuv 文檔啊,要不就翻船了,偶的大神之路也就夭折了不是。

http://docs.libuv.org/en/v1.x/check.html#c.uv_check_start

uv_idle_t — Idle handle

int uv_check_start(uv_check_t* check, uv_check_cb cb)uv_check_t

Start the handle with the given callback.

int uv_idle_start(uv_idle_t* idle, uv_idle_cb cb)uv_idle_t — Idle handle

Start the handle with the given callback.

哈哈哈哈 好的得意~~

我擦,等等!! Start the handle with the given callback. 倆都這個啊!我讀書少你沒騙我吧……

都告訴我是給evloop的回調句柄,區別呢大哥!

幸虧老子眼尖啊!倆受參數不一樣么,一個是 uv_check_t 另一個是 uv_idle_t,那就再查查唄。

額,原來就在本頁有啊……

先看 uv_check_t 好了,人家這麼說的:

Check handles will run the given callback once per loop iteration, right after polling for i/o.

哦哦,是在 io 輪詢之後執行回調啊。

接著看 uv_idle_t:

Idle handles will run the given callback once per loop iteration, right before the uv_prepare_t handles.

額,在 uv_prepare_t 之前…… 這貨又是什麼……

好在咱還是會查文檔滴:

uv_prepare_t

這裡說:

Prepare handles will run the given callback once per loop iteration, right before polling for i/o.

uv_prepare_t 會在 io 輪詢之前執行回調呢。

這就清楚了,掛在 uv_idle_start 里的 callback 會比掛在 uv_check_start 里的靠前執行。他倆中間估摸著是 io 輪詢的 callback 執行。

所以吧,第二個不同點就出現了——nextTick 用 uv_idle_start 掛的,它的callback function 最早被call,然後 io 的,最後是 setImmediate 的 callback function 被 call。

費那麼大勁兒,偶閑的想抬杠的其實是,人家裡頭沒有隊這種數據結構,也就沒加到隊首和隊尾一說…… 其實看起來是寫死的代碼執行順序而已。


正好最近在看 node 源碼,這個問題答案都有點歷史了,我來刷新一下。

先說答案:

process.nextTick 一般是在 poll 階段被執行,也有可能在 check 階段執行。

setImmediate 一般在 check 階段執行,也有可能在 poll 階段執行。

但總體來說 process.nextTick 和 setImmediate 關係不大。

setImmediate

setImmediate, setTimeout, setInterval 的實現大部分是在 /lib.timer.js。setImmediate 是這三個實現里最簡單的一個。

exports.setImmediate = function(callback, arg1, arg2, arg3) {
if (typeof callback !== "function") {
throw new TypeError(""callback" argument must be a function");
}
// ... 省略
// 前面主要工作是參數的判斷和包裝,在這裡開始創建 `Immediate`
return createImmediate(args, callback);
};

創建 Immediate 任務節點加入到 immediate 隊列

function createImmediate(args, callback) {
var immediate = new Immediate();
immediate._callback = callback;
immediate._argv = args;
immediate._onImmediate = callback;

if (!process._needImmediateCallback) {
process._needImmediateCallback = true;
process._immediateCallback = processImmediate;
}

immediateQueue.append(immediate);

return immediate;
}

在來看看 setImmediate 回調隊列的執行部分,可以看到這裡 immediateQueue 的遍歷沒有像process.nextTick 和 uv 的 event loop 中的單次執行數量限制。

function processImmediate() {
// 取隊列的頭尾,申明 `domain` 也就是域
var immediate = immediateQueue.head;
var tail = immediateQueue.tail;
var domain;

// 清空隊列頭尾
immediateQueue.head = immediateQueue.tail = null;

while (immediate) {
// immediate 任務的域
domain = immediate.domain;

// 如果沒有回調就下一個
if (!immediate._onImmediate) {
immediate = immediate._idleNext;
continue;
}

if (domain)
domain.enter();
// 不是很明白這裡,之前不是給它倆都賦值了 `callback` 么
immediate._callback = immediate._onImmediate;

// 先暫存一個下一個節點,避免 `clearImmediate(immediate)` 被調用時被清理。
var next = immediate._idleNext;

tryOnImmediate(immediate, tail);

if (domain)
domain.exit();

// 如果有調用 `clearImmediate(immediate)` 的話就使用之前暫存的next,沒有的話,那就調用 `immediate._idleNext`
if (immediate._idleNext)
immediate = immediate._idleNext;
else
immediate = next;
}

// 判斷 immediate 隊列為空的話設置 `_needImmediateCallback ` 標誌為false
// 需要提到的是這裡的邏輯 C++ 模塊中有實現
if (!immediateQueue.head) {
process._needImmediateCallback = false;
}
}

在來看執行部分

function tryOnImmediate(immediate, oldTail) {
var threw = true;
try {
// 這裡是因為之前的 v8 會放棄優化帶有`try/finally`的function,所以這裡把執行函數再外置到一個小函數,small function 會得到v8優化
runCallback(immediate);
threw = false;
} finally {
// 如果執行成功並且有下一個節點
if (threw immediate._idleNext) {
// 處理正常的話,繼續下一個
const curHead = immediateQueue.head;
const next = immediate._idleNext;

if (curHead) {
curHead._idlePrev = oldTail;
oldTail._idleNext = curHead;
next._idlePrev = null;
immediateQueue.head = next;
} else {
immediateQueue.head = next;
immediateQueue.tail = oldTail;
}
// 下一個事件循環中繼續處理 Immediate 任務隊列
process.nextTick(processImmediate);
}
}
}

看到最後的 process.nextTick(processImmediate), interesting Immediate 也有可能和 process.nextTick 中的任務一起執行。

脫離 JS 部分,看看回調是怎麼來被調用的。

MakeCallback(env, env-&>process_object(), env-&>immediate_callback_string()) 執行 process._immediateCallback 回調

static void CheckImmediate(uv_check_t* handle) {
Environment* env = Environment::from_immediate_check_handle(handle);
HandleScope scope(env-&>isolate());
Context::Scope context_scope(env-&>context());
MakeCallback(env, env-&>process_object(), env-&>immediate_callback_string());
}

獲取當前 Isolate 內的上下文線程上的 _needImmediateCallback flag

auto need_immediate_callback_string =
FIXED_ONE_BYTE_STRING(env-&>isolate(), "_needImmediateCallback");
CHECK(process-&>SetAccessor(env-&>context(), need_immediate_callback_string,
NeedImmediateCallbackGetter,
NeedImmediateCallbackSetter,
env-&>as_external()).FromJust());

static void NeedImmediateCallbackSetter(
Local& property,
Local& value,
const PropertyCallbackInfo& info) {
Environment* env = Environment::GetCurrent(info);

uv_check_t* immediate_check_handle = env-&>immediate_check_handle();
bool active = uv_is_active(
reinterpret_cast&(immediate_check_handle));

if (active == value-&>BooleanValue())
return;

uv_idle_t* immediate_idle_handle = env-&>immediate_idle_handle();

if (active) {
uv_check_stop(immediate_check_handle);
uv_idle_stop(immediate_idle_handle);
} else {
uv_check_start(immediate_check_handle, CheckImmediate);
// Idle handle is needed only to stop the event loop from blocking in poll.
uv_idle_start(immediate_idle_handle, IdleImmediateDummy);
}
}

這裡和上面 @貘吃饃香 貘大分析的類似

uv 文檔:

c:function:: int uv_check_start(uv_check_t* check, uv_check_cb cb)
Start the handle with the given callback.

c:function:: int uv_idle_start(uv_idle_t* idle, uv_idle_cb cb)
Start the handle with the given callback.

IdleImmediateDummy 實際上是這樣的,所以忽略 uv_idle_start

static void IdleImmediateDummy(uv_idle_t* handle) {
// Do nothing. Only for maintaining event loop.
// TODO(bnoordhuis) Maybe make libuv accept nullptr idle callbacks.
}

這裡可以認為 setImmediate 已經清晰了, check 階段調用執行數據結構為鏈表的回調隊列,異常情況的話,會在 nextTick 隊列中出現。

process.nextTick

看完 setImmediate,來看 process.nextTick

function setupNextTick() {
// 設置 Promise 模塊的調度方法
const promises = require("internal/process/promises");
const emitPendingUnhandledRejections = promises.setup(scheduleMicrotasks);

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

// Used to run V8"s micro task queue.
var _runMicrotasks = {};

// *Must* match Environment::TickInfo::Fields in src/env.h.
var kIndex = 0;
var kLength = 1;

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

// This tickInfo thing is used so that the C++ code in src/node.cc
// can have easy access to our nextTick state, and avoid unnecessary
// calls into JS land.

// 通過 process._setupNextTick 註冊 _tickCallback, 獲取 _runMicrotasks

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

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

...
}

_tickCallback _tickDomainCallback

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

第一個不同出現—— nextTick 有調用深度限制 1000 個,setImmediate 就木有~

這裡的數量限制就和貘大之前分析的 node 0.10.33 不太一樣,變得更大。

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

do {
while (tickInfo[kIndex] &< tickInfo[kLength]) { tock = nextTickQueue[tickInfo[kIndex]++]; callback = tock.callback; args = tock.args; // Using separate callback execution functions allows direct // callback invocation with small numbers of arguments to avoid the // performance hit associated with using `fn.apply()` _combinedTickCallback(args, callback); if (1e4 &< tickInfo[kIndex]) tickDone(); } tickDone(); _runMicrotasks(); emitPendingUnhandledRejections(); } while (tickInfo[kLength] !== 0); }

SetupNextTick

通過上面的 JS 部分我們了解到,process.nextTick, Microtasks 以及 Promise 的 callback 都是通過一個隊列 nextTickQueue 調度, 而這一切都是從
_tickCallback ( _tickDomainCallback )開始的。

src/node.cc

void SetupNextTick(const FunctionCallbackInfo& 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&());

// 將傳過來的 _runMicrotasks ({}) 對象添加 runMicrotasks 方法
env-&>SetMethod(args[1].As&(), "runMicrotasks", RunMicrotasks);

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

// Values use to cross communicate with processNextTick.
// 返回 tick_info 用於和 processNextTick js部分能同步狀態

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

Local& 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));

}

AsyncWrap

// src/async-wrap.cc

Local& AsyncWrap::MakeCallback(const Local& cb,
int argc,
Local&* argv) {

...

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

...

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

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

Local& 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&();
}

return ret;
}

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

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

// If someone handled it, then great. otherwise, die in C++ land
// since that means that we"ll exit the process, emit the "exit" event
if (!caught) {
try {
if (!process._exiting) {
process._exiting = true;
process.emit("exit", 1);
}
} catch (er) {
// nothing to be done about it at this point.
}

// if we handled an error, then make sure any ticks get processed
} else {
NativeModule.require("timers").setImmediate(process._tickCallback);
}
return caught;
};
}

大體上來說 process.nextTick 是在 poll 階段被執行,也有可能在 Check 階段執行。

你全看完算我輸。


Process.nextTick 是 micro task,setImmediate 從來沒有標準化但曾經有過的實現都是 task,所以本質上是 micro task 和 task 的區別:Tasks, microtasks, queues and schedules。


昨天pw說,nextTick是加到隊列頭,setImmediate是加到隊列尾。

僅轉發,不承擔責任……


先看兩段代碼,左邊是setImmediate,右邊是process.nextTick。然後再來看下兩段代碼的執行結果。所使用nodejs的版本是v0.10.26。

setImmediate的執行結果:

process.nextTick的執行結果:

在執行process.nextTick時,setTimeout永遠是最後執行。而在執行setImmedia時,setTimeout是隨機的插入在setImmediate的順序中的。

好,來說下兩個具體的不同吧。在具體說之前,先要明確一點,setTimeout中的callback是在nodejs的event loop中觸發執行的。

  • process.nextTick的所要執行的代碼回立即執行,並且是在返回nodejs的event loop之前。所以在process.nextTick的例子中,所有process.nextTick的函數會依次執行,執行結束之後,回到nodejs的event loop,然後再執行setTimeout。

  • 而setImmediate的執行呢,是被event loop觸發的。setImmediate中要執行的函數被放入一個隊列,每次迭代的時候,就會觸發隊列中當前第一個callback執行。所以執行順序會是A-B-C-D-E-F-G。這樣就防止了event loop被block住,允許其它的I/O或者timer的callback執行,所以setTimeout的執行會穿插在setImmediate的執行中。

以上是我所理解的process.nextTick和setTimeout。歡迎各位指出不足之處,謝謝。

附上,nodejs官方的說明:

  • Node v0.10.0 (Stable)


process.nextTick 註冊的回調會在事件循環的當前階段結束前執行,而不是只有 pollcheck 階段才會執行。process 是內核模塊,運行時是全局上下文,所以 microtask 只有一個,無論你是在哪個階段、哪個閉包內用 nextTick 註冊的回調都會被 push 到 nextTickQueue,並在事件循環當前階段結束前執行。

setImmediate 註冊的回調會在 check 階段、check 階段、check 階段執行。因為它需要由 check watcher 來執行,check watcher 只在 check 階段處於 active 狀態process.nextTick 不同,setImmediate 因運行時的上下文不同而產生不同的 ImmediateList,所以 macrotask 可以有多個。setImmediate 會在異常的時候執行 process.nextTick(processImmediate),會在當前階段結束前重新執行一次這個異常任務(即 check 階段)。

具體情況可以體驗一下下面代碼的輸出:

/**
* 執行棧中註冊 setTimeout 計時器
*/
setTimeout(function () {
// 4. timers 階段。timer watcher 遍歷計時器 Map 的 key,
// 如果有 key &<= timeout,執行該 key 對應的 value(計時器任務); // 否則等到 poll 階段再檢查一次 console.log("setTimeout"); setTimeout(function () { // 11. 註冊 setTimeout 計時器。UV_RUN_ONCE 模式下, // 會在循環結束之前再執行時間下限到達的計時器任務,取決於進程性能 // 1 &<= TIMEOUT_MAX &<= 2 ^ 31 - 1 console.log("setTimeout in setTimeout"); }, 0); setImmediate(function () { // 9. 註冊 setImmediate 計時器。在當前循環的 check 階段執行。 // (註:這是新的 ImmediateList,當前循環內有 3 個 ImmediateList 了) console.log("setImmediate in setTimeout"); }); process.nextTick(function () { // 6. 為 nextTickQueue 添加任務,timers 階段結束前喚醒 idle watcher // idle watcher 檢查 nextTickQueue,執行任務 console.log("nextTick in setTimeout"); }); }, 0); /** * 執行棧中註冊 setImmediate 計時器 */ setImmediate(function () { // 7. poll 階段沒有可執行任務,階段結束前喚醒 idle watcher,idle watcher 繼續睡; // 接著喚醒 check watcher,檢測到 ImmediateList 不為空,進入 check 階段。 // check watcher 執行第一個任務 console.log("setImmediate"); setTimeout(function () { // 13. 註冊 setTimeout 計時器 // 由於機器性能,在循環結束前才執行 console.log("setTimeout in setImmediate"); }, 0); setImmediate(function () { // 12. 為當前 ImmediateList 添加任務 // 由於機器性能優越,前面 nextTickQueue 為空了,直接進入 check 階段 console.log("setImmediate in setImmediate"); }); process.nextTick(function () { // 10. 為 nextTickQueue 添加任務,當所有 ImmediateList 的隊首任務都執行完畢時, // 喚醒 idle watcher,檢查 nextTickQueue,執行隊列任務 console.log("nextTick in setImmediate"); }); }); /** * 執行棧中為 nextTickQueue 添加任務 */ process.nextTick(function () { // 2. 執行棧為空,進入事件循環準備階段,喚醒 prepare watcher, // 檢查 nextTickQueue,執行隊列中的任務 console.log("nextTick"); setTimeout(function () { // 5. 註冊計時器任務,timers 階段到達時間下限則執行該任務, // 否則等到 poll 階段 console.log("setTimeout in nextTick"); }, 0); setImmediate(function () { // 8. 註冊 setImmediate 計時器,在當前循環的 check 階段執行。 // (註:這是新的 ImmediateList,當前循環內有 2 個 ImmediateList 了) console.log("setImmediate in nextTick"); }); process.nextTick(function () { // 3. prepare watcher 處於活躍狀態,檢測 nextTickQueue 的新任務, // 執行完所有任務後沉睡 console.log("nextTick in nextTick"); }); }); console.log("main thread"); // 1. 執行棧的任務


其實, 這個問題官方已經有解釋的很清楚了: The Node.js Event Loop, Timers, and process.nextTick() | Node.js

簡單的來說:

process.nextTick 在事件循環的當前階段處理完之後執行

setImmediate 只在事件循環 check 階段執行, poll 階段空閑時會檢測是否有 immediateTask 如果有的話則轉入 check 階段執行


本人寫過幾篇基於源碼分析的淺陋小文

http://mp.weixin.qq.com/s/B5RShyIqsfdASELlsQbSOA

可以看看


推薦閱讀:

如何評價 Node.js 的koa框架?
Object.create Reflect.setPrototypeOf 哪個比較好?
js中什麼技術能合併多個前端請求,並生成一個json文件發送?
Node.js 都應用在什麼項目上?這些項目為什麼選擇 Node.js?
如何利用mongodb+node.js完成一個搜索的功能?

TAG:Nodejs |