Nodejs Event Loop
Posted 前端花园
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Nodejs Event Loop相关的知识,希望对你有一定的参考价值。
环境:
Mac OS 10.15.7
Nodejs v14.16.0
Nodejs 是一个单进程,单线程架构。擅长IO密集型服务。单线程如何处理IO密集型任务的秘诀就是:async io。而正是 Event Loop 使 async io 成为可能。
Event Loop 有三个相关的API:setTimeout,setImmediate,process.nextTick;和一个状态迁移规则。
上图每个 box 认为是一个 phase,每个phase 关联一个 FIFO queue of event callbacks。
Event Loop 在执行每个phase 特有的任务后,依次执行callbacks直到执行完所有callbacks,或超过 callbacks 的最大个数限制。这时Event Loop 执行下一个 phase。
注意:每个callback 可以 schedule更多的不同 phase 的 callbacks。poll task是由kernel产生加入到队列中的。
下面对各phase做简单介绍:
timers |
执行setTimeout 和 setInterval schedule 的 callback。 过 threadhold 后尽快执行,而不是刚好在 threshold 时执行。 |
pending callbacks |
执行deferred 到 下一个 event loop iteration 的 IO callback。 执行 some system operation 的 callback。 如果有过多的(超过处理最大限制)http request,未处理的部分,会 deferred 到下次 event loop 的 pending phase 处理。 |
idle,prepare |
只内部使用 |
poll |
获取新的IO event,执行IO event 的 callback(除了 close event) 有两个作用:
|
check |
执行 setImmediate 回调 |
close |
执行 close callback。 如果socket 或 handle 被非正常关闭,close event 会在这个phase触发,否则会在 process.nextTick 中触发。 |
注意:当poll queue 为空,
1. 若 setImmediate 的 callback,则立即执行 setImmediate 的 callback,不会 block,否则 poll phase 会 block。
2. 若处理完 setImmediate,会检测 timer threadhold 是否到达,若到达则返回到 timers phase。
这里可以看到,poll phase 为空时会有两种情况,有setImmediate script时,执行 check phase,没有setImmediate script 时,检查 timer threshold,检测到会跳转为 timer phase。
基本概念介绍到这里,下面讲一些结论。
如果 setTimeout(fn, 0) 和 setImmediate 在 非 IO context 中,他俩的执行先后顺序是不确定的。
如果 setTimeout(fn, 0) 和 setImmediate 在 IO context 中,setImmediate 总是先执行。
process.nextTick 会 打断各个phase callbacks queue的调用,立即执行,因此 process.nextTick 可以向同一层级的 setTimeout 和 setImmediate加入回调。Nodejs 文档中提到的:current operation complete,指的是一个从 c++ handler 到 javascript callback 的一个 transition。
process.nextTick 的嵌套会立刻递归调用。
这里引入两个概念:
优先级
异步调用层级
如下面
main module 的优先级最高 设为:0
process.nextTick 的优先级次之设为:1
非 IO context 下 setTimeout 和 setImmediate 优先级相同,设为:2
IO context 下 setImmediate 优先级设为:2,setTimeout优先级设为:3
event loop 会按照优先级执行同一层级的异步事件回调,然后执行下一层级的异步事件回调。
一个异步调用,认为是一个层级,两个异步调用嵌套认为是两个层级,n 个异步调用层级,认为是n个层级。
有一个例外:process.nextTick 对内部的其他异步调用(setTimeout,setImmediate)不产生额外层级。
console.log("common code 1 here");
setTimeout(() => {
console.log("setTimeout 1");
}, 0);
setImmediate(() => {
console.log("setImmediate 1");
});
process.nextTick(() => {
console.log("nextTick 1");
});
console.log("common code 2 here");
输出:
分析:
main module 中的 common code 1 here 和 common code 2 here (优先级0)最先执行。
process.nextTick 随后执行(优先级1)
setTimeout 和 setImmediate 最后执行
所有代码在一个 event loop iteration 内完成。
如下 process.nextTick 中的 setTimeout,认为是第一层,全部代码在一个 event loop iteration 内完成
process.nextTick(() => {
setTimeout(() => {
console.log("setTimeout 2");
}, 0);
})
如果setImmediate 内部有嵌套:
setImmediate(() => {
console.log("setImmediate 1");
setImmediate(() => {
console.log("setImmediate 2");
});
});
则:setImmediate 2 会在第二个 event loop iteration内完成。
修改 process.nextTick部分,嵌套的两个 process.nextTick 会立即执行,并向setTimeout 的 callback queue内插入callback,所有动作还是在一个 event loop iteration内完成。
process.nextTick(() => {
console.log("nextTick 1");
process.nextTick(() => {
console.log("nextTick 2");
setTimeout(() => {
console.log("setTimeout 2");
}, 0);
});
});
如果将 nextTick 2 放入setTimeout 中,timers phase 会被打断,优先执行 nextTick 2 部分
输出:
console.log("common code 1 here");
setTimeout(() => {
console.log("setTimeout 1");
setTimeout(() => {
console.log("setTimeout 3");
}, 0);
}, 0);
process.nextTick(() => {
console.log("nextTick 1");
process.nextTick(() => {
console.log("nextTick 2");
setTimeout(() => {
console.log("setTimeout 2");
}, 0);
});
});
console.log("common code 2 here");
输出:
分析:
nextTick1 1 和 nextTick 2 连续输出。process.nextTick 的嵌套立即递归调用。
nextTick 2 后面的 setTimeout 和 setTimeout 1 是在同一层级上。
const fs = require("fs");
fs.readdir(__dirname, (err, data) => {
if (err) {
throw err;
}
setTimeout(() => {
console.log("setTimeout 1");
}, 0);
setImmediate(() => {
console.log("setImmediate 1");
});
process.nextTick(() => {
console.log("nextTick 1");
});
console.log("common data");
});
输出:
分析:
在 IO context 中 setImmediate 的优先级比setTimeout 高,仍低于process.nextTick。
在 process.nextTick 部分加入 setTimeout,整个代码还是在一个event loop iteration 内完成。
输出:
如果 setImmediate 内部再调用 setImmediate,和 process.nextTick 内的setTimeout 优先关闭不稳定。只有在第一层保证setImmediate 在 setTimeout 前执行。
const fs = require("fs");
fs.readdir(__dirname, (err, data) => {
if (err) {
throw err;
}
setTimeout(() => {
console.log("setTimeout 1");
}, 0);
setImmediate(() => {
console.log("setImmediate 1");
setImmediate(() => {
console.log("setImmediate 2");
});
});
process.nextTick(() => {
console.log("nextTick 1");
process.nextTick(() => {
console.log("nextTick 2");
process.nextTick(() => {
console.log("nextTick 3");
setTimeout(() => {
console.log("setTimeout 2");
});
});
});
});
console.log("common data");
});
输出:
分析:
IO context 中:
process.nextTick 嵌套会立即递归调用
process.nextTick 中的 setTimeout 和 setImmediate 2 的执行次数不确定。
process.nextTick demo
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
// use nextTick to emit the event once a handler is
process.nextTick(() => {
this.emit('event');
});
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
上面 MyEmitter constructor 中,如果 this.emit 不放在 process.nextTick 中,因此constructor 初始化时 还没有添加event 的 callback,所以没有输出。放入 nextTick 中,this.emit 的执行会延迟到 main module 代码执行完后,event loop 执行之前执行,这时event 已经注册了callback,所以有输出了。
类似的应用还有:
const server = net.createServer();
server.on('connection', (conn) => { });
server.listen(8080);
server.on('listening', () => { });
将 listening 事件的触发放入 process.nextTick 中,用户就可以注册 listening 的事件了。
将同步函数异步化:
function asyncWrapper(callback) {
process.nextTick(callback);
}
有几点需要声明下:
event loop 和 用户 code 共享同一个线程,而不是event loop 有单独一个线程
以上是关于Nodejs Event Loop的主要内容,如果未能解决你的问题,请参考以下文章
[未解决问题记录]python asyncio+aiohttp出现Exception ignored:RuntimeError('Event loop is closed')(代码片段