前段时间,我作了一个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
的概念并清晰了了解了各个方法的执行顺序,也算是收获满满了。有想法去实行才能得到成长真是真知灼见啊。