Node.js Event Loop
2019-04-28
第一次接触Node的时候就喜欢上它了,但是那时候学习它的时候,一直感觉入不了门,会写一点但总感觉隔了层膜,挺难受的,后来的渐渐有所感觉是在简单的了解了事件驱动后。再后来就觉得JS层面的Node其实没有太多的东西,于是开始想要去了解下层一点的东西。最近看了一篇非常好的博文,在这里做几点记录。
什么是事件循环
event Demultiplexer
来处理事件分发,同时把IO操作委托给硬件。它是一种抽象,各操作系统有自己的实现(Linux -> epoll,MacOS -> kqueue,Windows -> IOCP)。为了支持不同的操作系统中不同的IO操作,于是诞生了libuv。当IO操作被处理时,相对应的回调函数就被加入事件队列
事件队列执行并清空事件队列
循环上述过程
事件队列
简化的事件循环的执行阶段顺序
这里有几个要点:
有两个中间过渡检查:
process.nextTick
和process.resolve
,且前者优先级大于后者next tick 队列始终不为空导致IO饿死的问题
Timers
并不一定会准确执行,与CPU性能和当前所处的事件阶段有关两个很经典的例子
// 执行顺序不能保证
setTimeout(function() {
console.log('setTimeout');
}, 0);
setImmediate(function() {
console.log('setImmediate');
});
// setImmediate 一定在 setTimeout 之前执行
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
事件循环的阶段
当Node.js启动时会初始化event loop, 每一个event loop都会包含按如下顺序循环阶段:
Timer ——到期的定时器回调和 interval 回调。
Pending IO Callback——处理被挂起的 I/O 事件,包括完成的和失败的。
Idle —— 执行一些 libuv 内部操作。
Prepare —— 执行一些 I/O 操作的预准备工作。
Poll ——
可选择性地
等待 I/O 操作完成,这里可能会发生Node阻塞
。在node.js里,任何异步方法(除timer,close,setImmediate之外)完成时,都会将其callback加到poll queue里,并立即执行。所以Poll阶段:1.处理poll队列(poll quenue)的事件(callback); 2.当到达timers指定的时间时,执行timers的callback;
这里引用博客上的一段解释:
如果event loop进入了 poll阶段,且代码未设定timer,将会发生下面情况:
- 如果poll queue不为空,event loop将同步的执行queue里的callback,直至queue为空,或执行的callback到达系统上限;
如果poll queue为空,将会发生下面情况:
- 如果代码已经被setImmediate()设定了callback, event loop将结束poll阶段进入check阶段,并执行check阶段的queue (check阶段的queue是 setImmediate设定的)
如果代码没有设定setImmediate(callback),event loop将阻塞在该阶段等待callbacks加入poll queue;
如果event loop进入了 poll阶段,且代码设定了timer:
- 如果poll queue进入空状态时(即poll 阶段为空闲状态),event loop将检查timers,如果有1个或多个timers时间时间已经到达,event loop将按循环顺序进入 timers 阶段,并执行timer queue.
关于这里,我觉得 cNode上这个 [帖子](https://cnodejs.org/topic/57d68794cb6f605d360105bf) 的讨论非常精彩。
Check handlers —— 执行一些 I/O 操作的后续处理工作,通常来说,setImmediate 添加的回调也会在这个阶段执行。
Close handlers —— 执行一些 close 事件相关的操作比如 socket 连接等等。
此外,process.nextTick()不在event loop的任何阶段执行,而是在各个阶段切换的中间执行 事件轮询的核心代码
//deps/uv/src/unix/core.c
int uv_run(uv_loop_t *loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
//uv__loop_alive返回的是event loop中是否还有待处理的handle或者request
//以及closing_handles是否为NULL,如果均没有,则返回0
r = uv__loop_alive(loop);
//更新当前event loop的时间戳,单位是ms
if (!r)
uv__update_time(loop);
while (r != 0 && loop->stop_flag == 0) {
//使用Linux下的高精度Timer hrtime更新loop->time,即event loop的时间戳
uv__update_time(loop);
//执行判断当前loop->time下有无到期的Timer,显然在同一个loop里面timer拥有最高的优先级
uv__run_timers(loop);
//判断当前的pending_queue是否有事件待处理,并且一次将&loop->pending_queue中的uv__io_t对应的cb全部拿出来执行
ran_pending = uv__run_pending(loop);
//实现在loop-watcher.c文件中,一次将&loop->idle_handles中的idle_cd全部执行完毕(如果存在的话)
uv__run_idle(loop);
//实现在loop-watcher.c文件中,一次将&loop->prepare_handles中的prepare_cb全部执行完毕(如果存在的话)
uv__run_prepare(loop);
timeout = 0;
//如果是UV_RUN_ONCE的模式,并且pending_queue队列为空,或者采用UV_RUN_DEFAULT(在一个loop中处理所有事件),则将timeout参数置为
//最近的一个定时器的超时时间,防止在uv_io_poll中阻塞住无法进入超时的timer中
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
//进入I/O处理的函数(重点分析的部分),此处挂载timeout是为了防止在uv_io_poll中陷入阻塞无法执行timers;并且对于mode为
//UV_RUN_NOWAIT类型的uv_run执行,timeout为0可以保证其立即跳出uv__io_poll,达到了非阻塞调用的效果
uv__io_poll(loop, timeout);
//实现在loop-watcher.c文件中,一次将&loop->check_handles中的check_cb全部执行完毕(如果存在的话)
uv__run_check(loop);
//执行结束时的资源释放,loop->closing_handles指针指向NULL
uv__run_closing_handles(loop);
if (mode == UV_RUN_ONCE) {
//如果是UV_RUN_ONCE模式,继续更新当前event loop的时间戳
uv__update_time(loop);
//执行timers,判断是否有已经到期的timer
uv__run_timers(loop);
}
r = uv__loop_alive(loop);
//在UV_RUN_ONCE和UV_RUN_NOWAIT模式中,跳出当前的循环
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
//标记当前的stop_flag为0,表示当前的loop执行完毕
if (loop->stop_flag != 0)
loop->stop_flag = 0;
//返回r的值
return r;
}