[NodeJs系列]Q&A之理解NodeJs中的Event LoopTimers以及process.nextTick()
Posted 前端神盾局
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[NodeJs系列]Q&A之理解NodeJs中的Event LoopTimers以及process.nextTick()相关的知识,希望对你有一定的参考价值。
在上一篇文章中笔者提了几个问题,现在针对这些问题给出我的理解,如有错漏烦请指正。
poll阶段什么时候会被阻塞?
在上一篇文章中提到在poll阶段会“接收新的I/O事件并且在适当时node会阻塞在这里”,那什么情况下会阻塞呢?阻塞多久呢?
对于这个问题,我们必须深入到libuv的源码,看看poll阶段是怎么实现的:
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
r = uv__loop_alive(loop);
if (!r)
uv__update_time(loop);
while (r != 0 && loop->stop_flag == 0) {
uv__update_time(loop);
uv__run_timers(loop);
ran_pending = uv__run_pending(loop);
uv__run_idle(loop);
uv__run_prepare(loop);
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
// 这是poll阶段
uv__io_poll(loop, timeout);
uv__run_check(loop);
uv__run_closing_handles(loop);
if (mode == UV_RUN_ONCE) {
/* UV_RUN_ONCE implies forward progress: at least one callback must have
* been invoked when it returns. uv__io_poll() can return without doing
* I/O (meaning: no callbacks) when its timeout expires - which means we
* have pending timers that satisfy the forward progress constraint.
*
* UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from
* the check.
*/
uv__update_time(loop);
uv__run_timers(loop);
}
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
/* The if statement lets gcc compile it to a conditional store. Avoids
* dirtying a cache line.
*/
if (loop->stop_flag != 0)
loop->stop_flag = 0;
return r;
}
从源码我们可以看到 uv__io_poll
传入了 timeout
作为参数,而这个 timeout
就决定了poll阶段阻塞的时长,明白这一点我们就可以把问题转化成:是什么决定的 timeout
的值?
再回到源码中, timeout
的初始值为0,也就意味着poll阶段之后会直接转入check阶段而不会发生阻塞。但是当 (mode==UV_RUN_ONCE&&!ran_pending)||mode==UV_RUN_DEFAULT
这些条件成立时, timeout
就由 uv_backend_timeout
的返回值决定。
这里需要插播一下关于 mode
值的问题,根据官方文档 mode
一共有三种情况:
UV_RUN_DEFAULT
UV_RUN_ONCE
UV_RUN_NOWAIT
这里我们只关心 UV_RUN_DEFAULT
,因为Node event loop使用的是这种模式.
OK~回到问题,我们再看一下 uv_backend_timeout
会返回什么?
int uv_backend_timeout(const uv_loop_t* loop) {
if (loop->stop_flag != 0)
return 0;
if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
return 0;
if (!QUEUE_EMPTY(&loop->idle_handles))
return 0;
if (!QUEUE_EMPTY(&loop->pending_queue))
return 0;
if (loop->closing_handles)
return 0;
return uv__next_timeout(loop);
}
这是一个多步条件判断函数,我们一个个分析:
如果event loop已(或正在)结束(调用了
uv_stop()
,stop_flag!=0
),timeout
为0如果没有异步任务需要处理,
timeout
为0如果还有未处理的
idle_handles
和pending_queue
,timeout
为0(对于idle_handles
和pending_queue
分别代表什么,笔者还没有概念,如果后面有相应资料会及时更新)如果还有存在未清理的资源,
timeout
为0如果以上条件都不满足,则使用
uv__next_timeout
处理
int uv__next_timeout(const uv_loop_t* loop) {
const struct heap_node* heap_node;
const uv_timer_t* handle;
uint64_t diff;
heap_node = heap_min((const struct heap*) &loop->timer_heap);
if (heap_node == NULL)
return -1; /* block indefinitely */
handle = container_of(heap_node, uv_timer_t, heap_node);
if (handle->timeout <= loop->time)
return 0;
// 这句代码给出了关键性的指导
// 对比当前loop的时间戳
diff = handle->timeout - loop->time;
//不能大于最大的INT_MAX
if (diff > INT_MAX)
diff = INT_MAX;
return diff;
}
总结一下,event loop 满足以下条件时,poll阶段会进行阻塞:
event loop 并未触发关闭动作
还有异步队列没有处理
资源已全部关闭
而阻塞的时间最长不超过给定定时器的最小阀值
为什么在非I/O循环中,setTimeout
和setImmediate
的执行顺序是不一定的?
上文提到 setTimeout
和 setImmediate
在非I/O循环中,执行顺序是不一定的,比如:
setTimeout(function timeout() {
console.log('timeout');
}, 0);
setImmediate(function immediate() {
console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
相同代码,两次运行结果却是相反的,这是为什么呢?
在node中, setTimeout(cb,0)===setTimeout(cb,1)
在event loop的第一个阶段(timers阶段),node都会从一堆定时器中取出一个最小阀值的定时器来与 loop->time
进行比较,如果阀值小于等于 loop->time
表示定时器已超时,相应的回调便会执行(随后会检查下一个定时器),如果没有则会进入下一个阶段。
所以 setTimeout
是否在第一阶段执行取决于 loop->time
的大小,这里可能出现两种情况:
1. 由于第一次loop前的准备耗时超过1ms,当前的 loop->time>=1
,则 uv_run_timer
生效, timeout
先执行
2. 由于第一次loop前的准备耗时小于1ms,当前的 loop->time<1
,则本次loop中的第一次 uv_run_timer
不生效,那么 io_poll
后先执行 uv_run_check
,即 immediate
先执行,然后等 close cb
执行完后,继续执行 uv_run_timer
这就是为什么同一段代码,执行结果随机的缘故。那为什么说在I/O回调中,一定是先 immediate
执行呢,其实也很容易理解,考虑以下场景:
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
由于 timeout
和 immediate
的事件注册是在 readFile
的回调执行时触发的,所以必然的,在 readFile
的回调执行前的每一次event loop进来的 uv_run_timer
都不会有超时事件触发 那么当 readFile
执行完毕,poll阶段收到监听的fd事件完成后,执行了该回调,此时
timeout
事件注册immediate
事件注册由于
readFile
的回调执行完毕,那么就会从uv_io_poll
中出来,此时立即执行uv_run_check
,所以immediate
事件被执行掉最后的
uv_run_timer
检查timeout
事件,执行timeout
事件
所以你会发现,在I/O回调中注册的两者,永远都是 immediately
先执行
JS调用栈被展开是什么意思?
栈展开主要是指在抛出异常后逐层匹配catch语句的过程,举个例子:
function a(){
b();
}
function b(){
c();
}
function c(){
throw new Error('from function c');
}
a();
这个例子中,函数 c
抛出异常,这是首先会在 c
函数本身检查是否存在try相关的catch语句,如果没有就退出当前函数,并且释放当前函数的内存并销毁局部对象,继续到 b
函数中查找,这个过程就称之为栈展开。
参考
https://zhuanlan.zhihu.com/p/35039878
https://cnodejs.org/topic/57d68794cb6f605d360105bf
http://gngshn.github.io/2017/09/01/libuv-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-2-event-loop%E7%9A%84%E8%BF%90%E8%BD%AC/
http://docs.libuv.org/en/v1.x/design.html
以上是关于[NodeJs系列]Q&A之理解NodeJs中的Event LoopTimers以及process.nextTick()的主要内容,如果未能解决你的问题,请参考以下文章