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)


相关推荐

  • 使用SecureCRTPortable向Linux上传文件

    使用SecureCRTPortable向Linux上传文件1.打开SecureCRTPortable2.点击上册菜单栏文件->连接SFTP会话(S)3.上传文件sftp>put-r”本地文件目录\文件名”4.输入完成后点击回车,会将文件上传到Linux的当前用户的home目录下解析:如果你是用root连接的sftp,那么上传的文件就会保存到/root目录下…

  • 指令周期,时钟周期,总线周期概念辨析图_总线周期是指

    指令周期,时钟周期,总线周期概念辨析图_总线周期是指《指令周期、时钟周期、总线周期概念辨析》由会员分享,可在线阅读,更多相关《指令周期、时钟周期、总线周期概念辨析(2页珍藏版)》请在人人文库网上搜索。指令周期、时钟周期、总线周期概念辨析在计算机中,为了便于管理,常把一条指令的执行过程划分为若干个阶段,每一阶段完成一项工作。例如,取指令、存储器读、存储器写等,这每一项工作称为一个基本操作。完成一个基本操作所需要的时间称为机器周期。一般情况下,一个机器周期由若干个S周期(状态周期)组成。通常用内存中读取一个指令字的最短时间来规定CPU周期,(也就是计算机通

    2022年10月10日
  • 教你保存在线视频文件「建议收藏」

      当你看到很精彩的视频你想不想把他保存起来以后继续欣赏呢?或者是做成MP4格式放到手机里??但是目前绝大部分的视频网站由于版权、带宽等原因不提供视频下载服务,甚至想方设法把这些视频资源藏起来。所以你无法把它们保存到自己的电脑上。我们要怎么样才能把别人的视频文件保存到自己的电脑上呢?下面教你几招保存视频文件,让我们来突破封锁,把在线视频搬回家,想看就看!  一、WMV、ASF…

  • Django的HttpRequest[通俗易懂]

    Django的HttpRequest[通俗易懂]HttpReqeust对象服务器接收到http协议的请求后,会根据报文创建HttpRequest对象,这个对象不需要我们创建,直接使用服务器构造好的对象就可以。视图的第一个参数必须是HttpRequest对象,在django.http模块中定义了HttpRequest对象的API。属性下面除非特别说明,属性都是只读的。path:一个字符串,表示请求的页面的完整路径,不包含域名和参数部分。…

  • git命令-切换分支

    git命令-切换分支git一般有很多分支,我们clone到本地的时候一般都是master分支,那么如何切换到其他分支呢?主要命令如下

  • 经典算法–约瑟夫环问题的三种解法

    经典算法–约瑟夫环问题的三种解法约瑟夫环问题,这是一个很经典算法,处理的关键是:伪链表问题描述:N个人围成一圈,从第一个人开始报数,报到m的人出圈,剩下的人继续从1开始报数,报到m的人出圈;如此往复,直到所有人出圈。(模拟此过程,输出出圈的人的序号)在数据结构与算法书上,这个是用链表解决的。我感觉链表使用起来很麻烦,并且这个用链表处理起来也不是最佳的。我画了一个图用来理解:有如下问题需要首先考虑:1、“圈…

发表回复

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

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