为什么 setImmediate() 在 fs.readFile() 之前在 Nodejs 事件循环的作品中执行?

Why setImmediate() execute before fs.readFile() in Nodejs Event Loop's works?

看了很多相关的documents.But还是不明白是怎么回事

const fs = require('fs')
const now = Date.now();

setTimeout(() => console.log('timer'), 10);
fs.readFile(__filename, () => console.log('readfile'));
setImmediate(() => console.log('immediate'));
while(Date.now() - now < 1000) {
}

const now = Date.now();

setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timer'), 10);
while(Date.now() - now < 1000) {
}

我认为第一段代码应该记录:

readfile
immediate

第二段代码日志。

timer
immediate

我觉得还可以

问题: 不明白为什么第一段代码日志

immediate
readfile

我认为文件已经被完全读取并且它的回调函数enqueue I/O 1秒后回调phase的queque

然后我认为事件循环将移动到 timers(none)I/O callbacks(fs.readFile's callback)idle/prepare(none)poll(none)check(setImmediate's callback),最后是 close callbacks(none)按顺序,但结果是 setImmediate() 仍然是 运行 第一。

setTimeout(() => console.log('timer'), 10);                
fs.readFile(__filename, () => console.log('readfile'));    
setImmediate(() => console.log('immediate'));               

while(Date.now() - now < 1000) {
}

说明

  1. setTimeout 计划在 10 毫秒后放入事件循环。

  2. 异步文件读取开始。

  3. 非标准 setImmediate 计划显示控制台输出中断长进程。

  4. 一个一秒的阻塞循环运行s。控制台中还没有任何内容。

  5. setImmediate 在循环期间打印 immediate 控制台消息。

  6. 文件读取结束,回调在while循环后执行 超过。控制台输出 readfile 现在就在那里。

  7. 最后,控制台消息 timer 在大约 10 秒后打印出来。

注意事项

  • None 以上命令(循环除外)是同步的。他们 安排一些事情并立即执行下一个命令。

  • 回调函数只在当前阻塞后调用 执行完毕

  • 超时指令不保证在指定时间执行 间隔。保证他们 运行 在 之后 区间.

  • setImmediate 非常具有实验性。

您看到的行为是因为事件循环中有多种类型的队列,系统 运行s 事件根据它们的类型排列顺序。它不仅仅是一个巨大的事件队列,其中所有内容 运行 都根据添加到事件队列的时间按 FIFO 顺序排列。相反,它喜欢 运行 一种类型的所有事件(达到限制),前进到下一种类型,运行 所有这些等等。

而且,I/O 事件仅在循环中的一个特定点添加到它们的队列中,因此它们被迫按特定顺序排列。这就是 setImmediate() 回调在 readFile() 回调之前执行的原因,即使在 while 循环完成时两者都准备好了。

And then I think event loop will move to timers(none),I/O callbacks(fs.readFile's callback),idle/prepare(none),poll(none),check(setImmediate's callback)and finally close callbacks(none) in order, but result is that setImmediate() still run first.

问题是事件循环的 I/O 回调阶段 运行s I/O 回调已经在事件队列中,但它们没有被放入事件完成后自动排队。相反,它们只会在稍后的 I/O poll 步骤中放入事件队列(见下图)。所以,第一次通过 I/O 回调阶段,还没有 I/O 回调到 运行,因此你没有得到 readfile 输出,当你认为你会.

但是,setImmediate() 回调在事件循环中第一次准备就绪,因此它在 readFile() 回调之前到达 运行。

I/O 回调的延迟添加可能解释了为什么您对 readFile() 回调发生在最后而不是 setImmediate() 回调之前发生感到惊讶。

while 循环结束时会发生以下情况:

  1. 当 while 循环结束时,它从计时器回调开始,并看到计时器已准备好 运行 所以它 运行 就是这样。
  2. 然后,它转到 运行 任何 I/O 已经存在的回调,但还有 none。来自 readFile() 的 I/O 回调尚未收集。它将在本周期稍后收集。
  3. 然后,它会经历其他几个阶段并进入 I/O 轮询。收集 readFile() 回调事件并将其放入 I/O 队列(但还没有 运行)。
  4. 然后,它进入 checkHandlers 阶段,其中 运行 是 setImmediate() 回调。
  5. 然后,它再次开始事件循环。没有计时器,所以它转到 I/O 回调,它最终找到并 运行s readFile() 回调。

因此,让我们为那些不熟悉事件循环过程的人更详细地记录一下您的代码中实际发生的事情。当您 运行 此代码(在输出中添加了计时)时:

const fs = require('fs')

let begin = 0;
function log(msg) {
    if (!begin) {
        begin = Date.now();
    }
    let t = ((Date.now() - begin) / 1000).toFixed(3);
    console.log("" + t + ": " + msg);
}

log('start program');

setTimeout(() => log('timer'), 10);
setImmediate(() => log('immediate'));
fs.readFile(__filename, () => log('readfile'));

const now = Date.now();
log('start loop');
while(Date.now() - now < 1000) {}
log('done loop');

你得到这个输出:

0.000: start program
0.004: start loop
1.004: done loop
1.005: timer
1.006: immediate
1.008: readfile

我添加了相对于程序启动时间的计时(以秒为单位),这样您就可以看到程序何时执行。

情况如下:

  1. 计时器已启动并设置为从现在起 10 毫秒,其他代码继续 运行
  2. fs.readFile()开始运行,其他代码继续运行
  3. setImmediate()注册到事件系统中,其事件在相应的事件队列中,其他代码继续运行
  4. while循环开始循环
  5. while 循环期间,fs.readFile() 完成其工作(运行ning 在后台)。它的事件已准备就绪,但尚未在适当的事件队列中(稍后会详细介绍)
  6. while 循环在 1 秒后结束,Javascript 的初始序列完成,returns 返回系统
  7. 解释器现在需要从事件循环中获取 "next" 事件。但是,并非所有类型的事件都得到平等对待。事件系统有一个特定的顺序来处理队列中不同类型的事件。在我们的例子中,这里首先处理定时器事件(我将在下文中对此进行解释)。系统检查是否有任何计时器具有 "expired" 并准备好调用它们的回调。在这种情况下,它发现我们的计时器已 "expired" 并准备就绪。
  8. 调用计时器回调,我们看到控制台消息 timer
  9. 没有更多的计时器,因此事件循环进入下一阶段。事件循环的下一阶段是 运行 任何未决的 I/O 回调。但是,事件队列中还没有待处理的 I/O 回调。即使 readFile() 现在已经完成,它还没有在队列中(解释即将到来)。
  10. 然后,下一步是收集所有已完成的 I/O 事件,并让它们准备好 运行。在这里,readFile() 事件将被收集(虽然还没有 运行)并放入 I/O 事件队列。
  11. 然后下一步是 运行 任何待处理的 setImmediate() 处理程序。当它这样做时,我们得到输出 immediate.
  12. 然后,事件处理的下一步是运行任何关闭处理程序(这里有none到运行)。
  13. 然后,事件循环通过检查计时器重新开始。 运行.
  14. 没有待处理的计时器
  15. 然后,事件循环 运行 发送任何未决的 I/O 回调。这里是 readFile() 回调 运行s,我们在控制台中看到 readfile
  16. 程序没有更多的事件等待,所以它会执行。

事件循环本身是一系列用于不同类型事件的队列,并且(有一些例外),每个队列在移动到下一个类型的队列之前被处理。这会导致事件分组(一组中的计时器,另一组中的挂起 I/O 回调,另一组中的 setImmediate() 等等)。它不是所有类型之间严格的FIFO队列。事件是组内的 FIFO。但是,所有挂起的计时器回调(达到一定限度以防止一种类型的事件无限期地占用事件循环)在其他类型的回调之前被处理。

你可以在这张图中看到基本结构:

来自this very excellent article。如果您真的想了解所有这些东西,请多读几遍这篇参考文章。

最初让我吃惊的是为什么 readFile 总是在最后。这是因为即使 readFile() 操作完成,它也不会立即放入队列中。相反,事件循环中有一个步骤,其中收集完成的 I/O 事件(将在事件循环的下一个循环中处理)并且 setImmediate() 事件在当前循环结束时处理I/O 个刚刚收集的事件。这使得 readFile() 回调在 setImmediate() 回调之后进行,即使它们都已准备好在 while 循环中进行。

此外,执行 readFile()setImmediate() 的顺序并不重要。因为它们都在 while 循环完成之前准备就绪,所以它们的执行顺序取决于事件循环的顺序,因为它 运行 是不同类型的事件,而不是它们完成的确切时间。


在第二个代码块中,删除 readFile() 并将 setImmediate() 放在 setTimeout() 之前。使用我的定时版本,应该是这样的:

const fs = require('fs')

let begin = 0;
function log(msg) {
    if (!begin) {
        begin = Date.now();
    }
    let t = ((Date.now() - begin) / 1000).toFixed(3);
    console.log("" + t + ": " + msg);
}

log('start program');

setImmediate(() => log('immediate'));
setTimeout(() => log('timer'), 10);

const now = Date.now();
log('start loop');
while(Date.now() - now < 1000) {}
log('done loop');

并且,它生成此输出:

0.000: start program
0.003: start loop
1.003: done loop
1.005: timer
1.008: immediate

解释类似(这次缩短了一点,因为之前解释了很多细节)。

  1. setImmediate() 已注册到适当的队列中。
  2. setTimeout()被注册到定时器队列中。
  3. while 循环 运行s 1000ms
  4. 代码执行完毕,returns控制权交还给系统
  5. 系统从以计时器事件开始的事件逻辑的顶部开始。我们之前启动的计时器现在已经完成,所以它 运行 是它的回调并记录 timer
  6. 没有更多的计时器,事件循环 运行s 通过其他几种类型的事件队列,直到它到达它 运行s setImmediate() 处理程序并记录 immediate.

如果您有多个项目计划在 I/O 回调中启动,例如:

// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});

然后,您会得到稍微不同的行为,因为当事件循环处于其周期的不同部分时,将安排 setTimeout()setImmediate()。在此特定示例中,setImmediate() 将始终在计时器之前执行,因此输出将是:

 immediate
 timeout

在上面的流程图中,您可以看到"run completed I/O handlers"步骤在哪里。因为 setTimeout()setImmediate() 调用将从 I/O 处理程序中安排,它们将在事件循环的 "Run completed I/O handlers" 阶段安排。按照事件循环的流程,setImmediate() 将在事件循环返回服务计时器之前的 "check handlers" 阶段得到服务。

如果 setImmediate()setTimeout() 被安排在事件循环中的不同点,那么计时器可能会在 setImmediate() 之前触发,这就是前面示例中发生的情况。因此,两者的相对时间取决于调用函数时事件循环所处的阶段。