前段時間,我作了一個node模塊node-multi-worker ,但願經過這個模塊讓node可以脫離單線程的限制,具體的使用能夠看一下上面的連接。其思路就是註冊任務後,分出子進程,而後在主進程須要執行任務時,向reactor子進程發送命令,而reactor收到命令後分配到worker子進程在執行完成後返回結果到主進程。這篇文章主要是爲了跟你們分享一下我在開發過程當中,遇到的一個問題,如何解決以及對相關知識的一個挖掘。node
在第一次完成了該工程後,我作了一些簡單的測試,好比在子進程執行的方法中作一些加減乘除或者字符運算,固然都是沒問題的。而對於一些異步的狀況,我經過bluebird的處理也可以處理,因而我開始嘗試起了aysnc/await的狀況,結果發現這個的執行只要遇到await,await後面的語句可以執行,可是在下面的語句就不再能執行了。這個狀況頓時讓我摸不着了頭腦,我一度覺得是v8內核中對於這種子進程的狀況不支持(確實v8對你fork出子進程的支持是有問題的,不過跟這個問題沒關,具體在模塊的Readme中提到了),因而看了v8內部對async/await的實現,並無什麼發現有跟子進程有什麼關係,可是卻讓個人思路多了一條路,原來我以前用的Promise一直是bluebird的,並無使用js原生的Promise,因而我經過原生的promise再來執行以前使用bluebird作的異步調用,此次果真也是卡主了,甚至是這樣不是異步的操做調用了Promise都會卡主:react
new Promise(function(resolve,reject){
resolve(1);
}).then(function(data){
console.log(data);
})
複製代碼
這個時候我意識到,這個問題多是在Promise身上,因而我查了Promise的規範文檔,這上面有這樣一句話:git
promise.then(onFulfilled, onRejected)
2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].
Here 「platform code」 means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a 「macro-task」 mechanism such as setTimeout or setImmediate, or with a 「micro-task」 mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or 「trampoline」 in which the handlers are called.
複製代碼
這段規範比較晦澀,不過能夠總結出一點,setTimeout和setImmediate屬於macro-task
,而promise的決議回調以及process.nextTick
的回調則是在micro-task
中執行的,因而我在v8.h中搜索關於microtask
的關鍵詞,果真被我找到了一個方法Isolate::RunMicrotasks
,這個時候我趕忙在個人代碼中,也就是子進程begin_uv_run
函數改爲這樣:github
bool more;
do {
more = uv_run(loop, UV_RUN_ONCE);
if (more == false) {
more = uv_loop_alive(loop);
if (uv_run(loop, UV_RUN_NOWAIT) != 0)
more = true;
}
Isolate::GetCurrent()->RunMicrotasks();
} while (more == true);
_exit(0);
複製代碼
這個時候,await後面的語句也執行了起來,Promise也不會出現then後的語句不執行的狀況了,但卻發現process.nextTick
仍是不能執行,因而我到了node的內核中尋求結果,看了一番恍然大悟,原來node的nextTick是本身實現的,並不在micro-task
中,只是經過代碼的方式實現了標準中的執行順序。下面咱們經過node的源碼來解釋一番這其中的問題以及我經過對這些的瞭解後作出的最後解決方案。bootstrap
要了解這個咱們首先來看看libuv的啓動函數uv_ru那種的代碼:promise
...
uv__update_time(loop);
uv__run_timers(loop);//處理timer
...
uv__io_poll(loop, timeout);//處理io事件
uv__run_check(loop); //處理check回調
...
if (mode == UV_RUN_ONCE) { uv__update_time(loop);
uv__run_timers(loop);//處理timer
}
複製代碼
能夠從上面看到,主要是三個大事件的順序,timer,io,check這樣的順序,timer固然就是調用咱們setTimeout
註冊回調所用,io天然就是處理咱們註冊的一些異步io任務,好比fs的讀取文件,以及網絡請求這些任務。而check中經過src/env.cc中的代碼網絡
uv_check_start(&immediate_check_handle_, CheckImmediate);
複製代碼
註冊了調用setImmediate
回調方法的CheckImmediate
函數。好了如今,setTimeout
和setImmediate
都找到了出處,那process.nextTick
和Promise.then
呢?這個答案就在uv__io_poll
中,由於咱們全部的io的回調函數最後都是經過 src/node.cc中的函數InternalMakeCallback
完成的,在其中經過這樣的語句來完成整個回調函數的調用過程:異步
...
InternalCallbackScope scope(env, recv, asyncContext);
...
ret = callback->Call(env->context(), recv, argc, argv);
...
scope.Close();
複製代碼
其中的scope.Close()
是執行process.nextTick
和Promise.then
的關鍵,由於它會執行到代碼:async
....
if (IsInnerMakeCallback()) {
//上一個scope還沒釋放不會執行
return;
}
Environment::TickInfo* tick_info = env_->tick_info();
if (tick_info->length() == 0) {
//沒有tick任務執行microtasks後返回
env_->isolate()->RunMicrotasks();
}
...
if (tick_info->length() == 0) {
tick_info->set_index(0);
return;
}
...
if (env_->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) {
//執行tick任務
failed_ = true;
}
複製代碼
從上面咱們能夠知道,在io任務註冊的callback執行完了之後便會調用tick任務和microtasks,其中env_->tick_callback_function()
就是lib/internal/process/next_tick.js中的函數_tickCallback
,其代碼:ide
do {
while (tickInfo[kIndex] < tickInfo[kLength]) {
...
_combinedTickCallback(args, callback);//執行註冊的回調函數
...
}
...
_runMicrotasks();//執行microtasks
...
}while (tickInfo[kLength] !== 0);
複製代碼
能夠看到在執行完process.nextTick
註冊的全部回調後,就會執行_runMicrotasks()
來執行microtask。這裏我不由產生了疑惑,回調我也執行了啊,爲什麼沒有執行process.nextTick
和microtask,惟一不會執行的狀況只能在這裏:
if (IsInnerMakeCallback()) {
//上一個scope還沒釋放不會執行
return;
}
複製代碼
帶着這個明確的目的,我找到了緣由所在,在src/node.cc中經過如下代碼來執行js代碼的:
{
Environment::AsyncCallbackScope callback_scope(&env);
env.async_hooks()->push_async_ids(1, 0);
LoadEnvironment(&env); //在這裏執行js
env.async_hooks()->pop_async_id(1);
}
複製代碼
在AsyncCallbackScope對象的構造函數中會執行以下語句:
env_->makecallback_cntr_++;
複製代碼
而IsInnerMakeCallback
判斷標準就是env_->makecallback_cntr_>1
,在callback_scope
析構時會將該值復原,可是咱們的子進程在js執行中就分配出來了,而且經過uv_run
後直接就exit因此並無機會析構該對象,固然沒法調用tick函數和microtask。不過確定有讀者如今產生疑惑了,那假如我不註冊io事件 只執行process.nextTick
和Promise.then
呢,從上面講解來看豈不是不能執行,可是我明明執行了的啊,莫急各位看官,由於還有個地方我還沒說到,就是node的js啓動文件lib/internal/bootstrap_node.js中的命令行交互式啓動使用的evalScript
方法仍是直接文件啓動的runMain
中都會在最後執行到_tickCallback
,也符合js語句執行也是macrotask的一種,在執行完js語句後第一時間執行microtask的原則。因此這個問題的結果就不言而喻了:
(function test() {
setTimeout(function() {console.log(4)}, 0);
new Promise(function executor(resolve) {
console.log(1);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
})()
複製代碼
首先js先執行因此確定1,2,3是按順序執行,而js執行到最後一步就是_tickCallback
,因此就是5,而執行完了js之後uv_run
,天然就是執行timer,固然在node中setTimeout的時間爲0時,實際爲1,因此在第一次調用uv__run_timers(loop);
不必定會執行,不過不影響這個函數的結果爲 1,2,3,5,4。而若是是這樣:
(function test() {
setTimeout(function() {console.log(1)}, 0);
setImmediate(function() {console.log(2)});
})()
複製代碼
順序就是不肯定的,理由已經講過了就是第一次timer的調用對time爲1的執行與否是不肯定的。
清楚了爲何不執行的緣由後解決該問題的方法就已經出來了,有兩個方法,一個是等js執行完了之後,再分出子進程,能夠經過註冊了一個timer任務來作,另一個天然就是在裏面分出,可是本身來作 tick
,我選擇了第二個方式,比較簡單粗暴,直接經過在子進程的函數中這樣寫:
bool more;
do {
more = uv_run(loop, UV_RUN_ONCE);
if (more == false) {
more = uv_loop_alive(loop);
if (uv_run(loop, UV_RUN_NOWAIT) != 0)
more = true;
}
v8::HandleScope scope(globalIsolate);
Local<Object> global = globalIsolate->GetCurrentContext()->Global();
Local<Object> process = Local<Object>::Cast(global->ToObject()->Get(String::NewFromUtf8(globalIsolate, "process")));
Local<Function> tickFunc = Local<Function>::Cast(process->ToObject()->Get(String::NewFromUtf8(globalIsolate, "_tickCallback")));
tickFunc->Call(process,0,NULL);
} while (more == true);
_exit(0);
複製代碼
這樣就不會再有問題了,經過_tickCallback
將tick回調和microtask都執行了。
經過這個模塊的開發,瞭解到了microtask
和macrotask
的概念並清晰了了解了各個方法的執行順序,也算是收穫滿滿了。有想法去實行才能得到成長真是真知灼見啊。