Node子进程async/await方法不正常执行的思考和解决

Node子进程async/await方法不正常执行的思考和解决

前段时间,我做了一个node模块node-multi-worker ,希望通过这个模块让node能够脱离单线程的限制,具体的使用可以看一下上面的链接。其思路就是注册任务后,分出子进程,然后在主进程需要执行任务时,向reactor子进程发送命令,而reactor收到命令后分配到worker子进程在执行完成后返回结果到主进程。这篇文章主要是为了跟大家分享一下我在开发过程中,遇到的一个问题,如何解决以及对相关知识的一个挖掘。

不执行的async/await

在第一次完成了该工程后,我做了一些简单的测试,比如在子进程执行的方法中做一些加减乘除或者字符运算,当然都是没问题的。而对于一些异步的情况,我通过bluebird的处理也能够处理,于是我开始尝试起了aysnc/await的情况,结果发现这个的执行只要遇到await,await后面的语句能够执行,但是在下面的语句就再也不能执行了。这个情况顿时让我摸不着了头脑,我一度以为是v8内核中对于这种子进程的情况不支持(确实v8对你fork出子进程的支持是有问题的,不过跟这个问题没关,具体在模块的Readme中提到了),于是看了v8内部对async/await的实现,并没有什么发现有跟子进程有什么关系,但是却让我的思路多了一条路,原来我之前用的Promise一直是bluebird的,并没有使用js原生的Promise,于是我通过原生的promise再来执行之前使用bluebird做的异步调用,这次果然也是卡主了,甚至是这样不是异步的操作调用了Promise都会卡主:

new Promise(function(resolve,reject){
        resolve(1);
}).then(function(data){
	console.log(data);
})
复制代码

这个时候我意识到,这个问题可能是在Promise身上,于是我查了Promise的规范文档,这上面有这样一句话:

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函数改成这样:

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的源码来解释一番这其中的问题以及我通过对这些的了解后做出的最后解决方案。

node中对macrotask和microtask的调用实现

要了解这个我们首先来看看libuv的启动函数uv_ru那种的代码:

...
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的关键,因为它会执行到代码:

....
 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,其代码:

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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/101371.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • c/c++ 学习-open 函数

    c/c++ 学习-open 函数转自:http://blog.csdn.net/dong_007_007/article/details/16269021open函数用于打开和创建文件。以下是open函数的简单描述      #include       intopen(constchar*pathname,intoflag,…/*mode_tmode*/);   

  • Tabnine 激活码破解方法【注册码】

    Tabnine 激活码破解方法【注册码】,https://javaforall.cn/100143.html。详细ieda激活码不妨到全栈程序员必看教程网一起来了解一下吧!

  • 细谈微商分销系统开发对企业的发展是好还是坏「建议收藏」

    细谈微商分销系统开发对企业的发展是好还是坏「建议收藏」  微商分销系统开发,,微商二级分销系统,微商系统开发,微商软件。微商APP开发,微商分销模式,微商全返系统,微商返利系统,微商管理系统。  从各个层面和角度,全方位解决微商品牌企业,在渠道中的一切微商管理问题,让微商品牌和团队轻松应对市场。  一、微商分销系统的特点  1.当你成为产品的代理商之后,你可以选择多种发货模式,平台或推广者可以选择发货模式。  …

  • vue组件注册可以是以下哪种方式_自定义vue组件分三个步骤

    vue组件注册可以是以下哪种方式_自定义vue组件分三个步骤组件的组织通常一个应用会以一棵嵌套的组件树的形式来组织:例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。为了能在模板中使用,这些组件必须先注册以便

  • host地址_hostname在哪个目录

    host地址_hostname在哪个目录https://laod.cn/hosts/hosts-position.html

  • 简述struts2拦截器的作用_springmvc登录拦截器

    简述struts2拦截器的作用_springmvc登录拦截器转自:http://developer.51cto.com/art/200906/126894.htm对struts2的连接器的解释还是挺不错的,分享一下:一、理解Struts2拦截器1.Struts2拦截器是在访问某个Action或Action的某个方法,字段之前或之后实施拦截,并且Struts2拦截器是可插拔的,拦截器是AOP的一种实现.2.拦截器栈(Int

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号