Node子進程async/await方法不正常執行的思考和解決

前段時間,我作了一個node模塊node-multi-worker ,但願經過這個模塊讓node可以脫離單線程的限制,具體的使用能夠看一下上面的連接。其思路就是註冊任務後,分出子進程,而後在主進程須要執行任務時,向reactor子進程發送命令,而reactor收到命令後分配到worker子進程在執行完成後返回結果到主進程。這篇文章主要是爲了跟你們分享一下我在開發過程當中,遇到的一個問題,如何解決以及對相關知識的一個挖掘。node

不執行的async/await

在第一次完成了該工程後,我作了一些簡單的測試,好比在子進程執行的方法中作一些加減乘除或者字符運算,固然都是沒問題的。而對於一些異步的狀況,我經過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

node中對macrotask和microtask的調用實現

要了解這個咱們首先來看看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函數。好了如今,setTimeoutsetImmediate都找到了出處,那process.nextTickPromise.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.nextTickPromise.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.nextTickPromise.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都執行了。

總結

經過這個模塊的開發,瞭解到了microtaskmacrotask的概念並清晰了了解了各個方法的執行順序,也算是收穫滿滿了。有想法去實行才能得到成長真是真知灼見啊。

相關文章
相關標籤/搜索