理解Node.js中Event Loop(四)

理解Node.js中Event Loop(四)

本文翻译至:Visualizing the I/O Queue in the Node.js Event Loop

欢迎阅读我们有关可视化 Node.js 事件循环系列的第四篇文章。在上一篇文章中,我们探讨了执行异步代码时的Timer队列及其优先级顺序。在本文中,我们将深入研究Input/Output出队列,这是另一个在事件循环中起着至关重要作用的队列。

在深入研究 I/O 队列之前,让我们快速回顾一下微任务和Timer队列。要将回调函数添加到微任务队列中,我们使用 process.nextTick()Promise.resolve() 等函数。在 Node.js 中执行异步代码时,Microtask 队列具有最高优先级

而要将回调函数添加到Timer队列中,我们使用 setTimeout()setInterval() 等函数。

目录

添加回调函数到队列

要将回调函数添加到 I/O 队列,我们​​可以使用Node.js 内置模块中的大多数异步方法。对于我们的实验,我们将使用 fs 模块中的 readFile() 方法。

实验6

首先,我们导入 fs 模块并调用 readFile() 方法。这会将回调函数添加到I/O队列。在 readFile() 之后,我们向 nextTick 队列添加一个回调函数,向 Promise 队列添加一个回调函数。

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"));

可视化:

执行完主线程内执行栈中的代码后,nextTick 队列、promise 队列和 I/O 队列中各有一个回调,此时主线程空闲,控制进入事件循环。

nextTick 队列具有最高优先级,其次是 Promise 队列,然后是 I/O 队列nextTick 队列中的第一个回调函数被弹出队列并执行,将消息log到控制台。

nextTick 队列为空时,事件循环将进入 Promise 队列。回调弹出队列并在执行栈上执行,将消息打印到控制台。

现在 Promise 队列现在为空,事件循环将继续到Timer队列。如果Timer队列中没有回调,则事件循环将进入 I/O 队列,该队列有一个回调。此回调将弹出队列并执行,从而在控制台上生成最终日志消息。

由实验6推断

微任务队列中的回调先于 I/O 队列中的回调执行。

对于我们的下一个实验,让我们将 微队列 与 Timer 队列交换

实验7

index.js
const fs = require("fs");

setTimeout(() => console.log("this is setTimeout 1"), 0);

fs.readFile(__filename, () => {
  console.log("this is readFile 1");
});

由于 setTimeout() 属于 Timer队列管理,且setTimeout()的延迟设置为0ms,此次执行将涉及Timer队列而不是微任务队列。即对比的两个项目为Timer队列和I/O队列

乍一看,预期的输出似乎很简单: setTimeout() 回调在 readFile() 回调之前执行。然而,事情并没有那么简单。这是运行同一段代码五次的输出

这种输出不一致的原因是,在使用 setTimeout() 设置延迟为 0 毫秒和 I/O 异步方法时,执行顺序的不可预测性。显而易见的问题是,“为什么无法保证执行顺序?”


该异常是由于计时器的最小延迟设置方式造成的。在 DOMTimer 的 C++ 实现中,我们遇到了一段非常有趣的代码。计算间隔以毫秒为单位,但上限为 1 毫秒 或 用户传递的间隔乘以 1 毫秒

DOMTimer

std::max(oneMillisecond, interval * oneMillisecond)

这说明,如果我们传入 0 毫秒,则间隔为 max(1,0),即设置为 1。这将导致 setTimeout1 毫秒的延迟。 Node.js 似乎遵循类似的实现。当您设置 0 毫秒延迟时,它将被覆盖为 1 毫秒延迟

但是 1ms 的延迟如何影响两个日志语句的顺序呢?

在事件循环开始时,Node.js 需要确定 1ms 的时间是否已达到。如果事件循环在 0.05ms 的时候进入Timer队列,而 1ms 的回调尚未添加到队列内,则控制权将移至 I/O 队列,执行 readFile() 回调。在事件循环的下一次迭代中,再执行Timer队列回调

另一方面,如果 CPU 比较"繁忙"并在 1.01 ms时进入Timer队列,则Timer队列将回调弹出并执行。然后控制将继续到 I/O 队列,并执行 readFile() 回调。

可以将Timer队列,I/O队列等想象成一个个"房间",事件循环就是一个"巡逻的人"。每次循环开始,"巡逻的人"都会一一敲开每个"房间"检查。有可能进入Timer房间(队列)的时候,那个setTimeout()回调还没有在"房间"内,所以认为Timer队列为空。就前往下一个房间(I/O队列)查看了。

个人疑问: 按理说,一旦进入事件循环后,说明主线程已经为空闲状态,上一轮同步代码已经执行结束。那么为什么将一个setTimeout()的回调添加到队列内需要这么"久"?


由于 CPU 繁忙程度的不确定性以及 0ms 延迟被覆盖为 1ms 延迟,我们永远无法保证 0ms 定时器和 I/O 回调之间的执行顺序

P.S. 其实哪怕设置为1ms,也无法保证setTimeoutreadFile()的执行顺序,关键在于,在此轮事件循环中,Timer队列内的回调是否早一步被添加和执行。

由实验7推断

当以 0ms 延迟和 I/O 异步方法运行 setTimeout() 时,永远无法保证执行顺序。

实验8

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);

for (let i = 0; i < 2000000000; i++) {}

该代码包含多个调用,这些调用将回调函数放入不同的队列中。

  • readFile() 调用将回调函数放入 I/O 队列

  • process.nextTick() 调用将其放入 nextTick 队列

  • Promise.resolve().then() 调用将其放入队列中在 Promise队列

  • setTimeout() 调用将其放入 Timer队列中。

为了避免之前实验中出现的任何计时器问题,我们添加了一个不执行任何操作的 for 循环。这确保了当控制进入计时器队列时, setTimeout() 计时器已经过去,并且回调已准备好执行

可视化:

为了直观地了解执行顺序,让我们分解一下代码中发生的情况。

当调用堆栈执行所有语句时,我们最终会在 nextTick 队列中得到一个回调,在 Promise 队列中得到一个回调,在Timer队列中得到一个回调,在 I/O 队列中得到一个回调。

主线程空闲,控制进入事件循环。

  1. nextTick队列中的第一个回调将出队并执行,并将消息log到控制台。

  2. 现在 nextTick 队列为空,事件循环将进入 Promise 队列。回调弹出队列并在调用堆栈上执行,在控制台中打印一条消息。

  3. 此时 Promise队列 为空,事件循环进入 Timer队列。回调函数出队并执行。

  4. 最后,事件循环进入 I/O 队列,其中有一个回调被弹出队列并执行,从而在控制台中生成最终的日志消息。

由实验8推断

I/O 队列回调在 微任务队列回调 和 Timer 队列回调 之后执行。


结论

实验表明,Input/Output 队列 中的回调是在 微任务队列中的回调和 Timer队列中的回调之后执行的。但当setTimeout()设置为0ms的延迟来运行,且与 I/O 异步方法一同执行时,两者执行顺序取决于 CPU 的繁忙程度。


参考:

What is the reason JavaScript setTimeout is so inaccurate?

Understanding setTimeout()


此系列文章:

理解Node.js中Event Loop(一)

理解Node.js中Event Loop(二)

理解Node.js中Event Loop(三)

理解Node.js中Event Loop(四)

理解Node.js中Event Loop(五)

理解Node.js中Event Loop(六)

理解Node.js中Event Loop(七)