视觉指南:理解 Node.js 事件循环中的 I/O 轮询
原文链接:Visualizing I/O Polling in the Node.js Event Loop,2023年4月6日,by Vishwas Gopinath
欢迎来到我们关于“可视化 Node.js 事件”循环的第五篇文章。在 上一篇文章 中,我们探讨了 I/O 队列及其在执行异步代码时的优先级顺序。在本文中,我们将继续专注于 I/O 队列,并逐渐介绍检查队列。需要注意的重要点将在下一个实验中进行解释。
入队回调函数
在我们继续实验之前,我想提一下,将回调函数排队到检查队列,我们使用内置的 setImmediate()
函数。语法很简单:setImmediate(callbackFn)
。当此函数在调用栈上执行时,回调函数将被排队到检查队列中。
注意:前八个实验涉及微任务、定时器和 I/O 队列,并已在先前的文章中介绍过。所有实验都是使用 CommonJS 模块格式运行的。
实验九
代码
// index.js
const fs = require("fs");
fs.readFile(__filename, () => {
console.log("this is readFile 1");
});
process.nextTick(() => console.log("this is process.nextTick 1"));
Promise.resolve().then(() => console.log("this is Promise.resolve 1"));
setTimeout(() => console.log("this is setTimeout 1"), 0);
setImmediate(() => console.log("this is setImmediate 1"));
for (let i = 0; i < 2000000000; i++) {}
译注:这段代码是在实验八的基础上增加了
setImmediate()
的函数调用。
代码片段继承自上一个实验。它包括对 readFile()
的调用,该调用将回调函数排队到 I/O 队列中;对process.nextTick()
的调用,该调用将回调函数排队到 nextTick 队列中;对 Promise.resolve().then()
的调用,该调用将回调函数排队到 Promise 队列中;以及对 setTimeout()
的调用,该调用将回调函数排队到计时器队列中。
在本次实验中引入了 setImmediate()
方法,并将其使用于检查队列来排除第七个实验所遇到的定时器问题。为确保当控制进入计时器队列时,setTimeout()
计时器已经准备好执行回调操作,我在代码里加入了长时间运行的 for
循环。
可视化
如果运行代码片段,可能会注意到输出不是我们想的那样。setImmediate()
的回调消息在 readFile()
的回调消息之前打印。以下是参考输出:
这可能看起来很奇怪,因为 I/O 队列出现在检查队列之前,但是一旦我们理解了两个队列之间发生的 I/O 轮询的概念,就知道原因了。为了帮助说明这个概念,看下可视化动图。
首先,所有函数都在调用栈上执行,回调被入队到对应的队列中。然而,readFile()
回调不会同时排队。让我来解释一下为什么。
当控制进入事件循环时,首先检查微任务队列中的回调。在这种情况下,nextTick 队列和 Promise 队列各有一个回调。nextTick 队列具有更高优先级,因此我们首先看到 "nextTick 1" 被记录,然后是 "Promise 1"。
两个队列都空了,控制移动到计时器队列。那里有一个回调,在控制台上记录 "setTimeout 1"。
现在来到了有趣的部分。当控制权达到 I/O 队列时,我们期望 readFile()
回调是有的对吗?毕竟我们有一个长时间运行的 for
循环,readFile()
应该已经完成了。
但实际上,事件循环会轮询检查 I/O 操作是否完成,并且只会将已完成的回调加入到队列中。这表示当控制权第一次进入 I/O 队列时,这个队列还是空的。
然后控制移动到事件循环的轮询部分, 检查 readFile()
任务是否完成。readFile()
确认完成了,事件循环将相关回调函数添加到 I/O 队列中。但是,执行已经越过了(moved past) I/O 队列,队列中回调必须等待再次轮到它时才会执行。
然后控制移动到检查队列,在那里找到一个回调。它记录 "setImmediate 1" 到控制台上,然后开始新的周期迭代,因为当前事件循环中没有其他要处理的内容了。
微任务和计时器队列都空了,但是 I/O 队列中有一个回调。这个回调就被执行了,并记录 "readFile 1" 到控制台上。
这就是为什么我们先看到 "setImmediate 1" 被记录而不是 "readFile 1" 的原因。实际上,在我们之前的实验中也有这种行为,但由于没有其他代码运行做干扰,我们就没有观察到它。
推论
I/O 完成前,I/O 事件会被轮询。直到 I/O 完成后, 才会将回调函数添加到 I/O 队列。
总结
调用 I/O 操作后,它的回调函数并不会立即排入 I/O 队列。在此期间,I/O 轮询阶段会检查 I/O 操作是否已经完成,并将已完成操作的回调排进队列中。这有时可能导致在执行 I/O 队列回调之前先执行检查队列回调。
然而,当两个队列都包含回调函数时,I/O 队列中的回调始终会先运行。在设计依赖于 I/O 回调以确保正确排序和执行回调的系统时,了解此行为非常重要。
继续阅读
- 第一部分:可视化 Node.js 事件循环
- 第二部分:可视化 Node.js 中的微任务队列
- 第三部分:可视化 Node.js 事件循环中的计时器队列
- 第四部分:可视化 Node.js 事件循环中的 I/O 队列
- 第五部分:可视化 Node.js 事件循环中的 I/O 轮询(本篇)
- 第六部分:可视化 Node.js 事件循环中的检查队列
- 第七部分:可视化 Node.js 事件循环中的关闭队列
转载自:https://juejin.cn/post/7225158744782684219