likes
comments
collection
share

视觉指南:理解 Node.js 事件循环中的 I/O 轮询

作者站长头像
站长
· 阅读数 63

原文链接:Visualizing I/O Polling in the Node.js Event Loop,2023年4月6日,by Vishwas Gopinath

视觉指南:理解 Node.js 事件循环中的 I/O 轮询 欢迎来到我们关于“可视化 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() 的回调消息之前打印。以下是参考输出:

视觉指南:理解 Node.js 事件循环中的 I/O 轮询

这可能看起来很奇怪,因为 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 回调以确保正确排序和执行回调的系统时,了解此行为非常重要。

继续阅读

转载自:https://juejin.cn/post/7225158744782684219
评论
请登录