【问题标题】:Why setImmediate() execute before fs.readFile() in Nodejs Event Loop's works?为什么在 Nodejs Event Loop 的作品中 setImmediate() 在 fs.readFile() 之前执行?
【发布时间】:2018-05-23 07:35:44
【问题描述】:

我已经阅读了很多相关的文档。但我仍然无法理解它是如何工作的。

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

我认为文件已经被完全读取,并且它的回调函数在 1 秒后入队 I/O 回调阶段的队列。

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

【问题讨论】:

  • 没有关于 1) 预期结果和重要的是 2) 问题/错误的真实陈述。 while 循环看起来也缺少主体。
  • 你想达到什么,你有什么问题?请澄清。
  • 日志之所以如此,是因为fs.readfile 是异步的,它不会阻塞 I/O。让setTimeout 执行。当文件被读取时,它会记录readfile
  • setImmediate也是异步的,我觉得应该在fs.readfile之前调用。

标签: javascript node.js event-loop


【解决方案1】:
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. 运行一秒钟的阻塞循环。控制台中还没有任何内容。

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

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

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

注意事项

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

  • 回调函数仅在当前阻塞后调用 执行结束。

  • 不保证超时命令在指定时间执行 间隔。保证它们将在任何时间之后运行 间隔。

  • setImmediate 非常具有实验性。

【讨论】:

  • 您的回答中有几个错误:1)它是 10 微秒而不是 10 秒 2)setImmediate()'s callbackwhile 循环结束后也被调用 while。(因为 immediatereadfile一起记录,文件很小。)
  • 已编辑毫秒。请参考 MDN 以查看 setImmediate 在循环期间触发。
  • 我已经阅读了整个答案和 cmets。最后为什么 I/o Phase(readFile()) ** 在 **Check Phase(setImmediate) 之后执行,因为 I/o 回调 不会在事件循环的当前循环中执行即使它已准备好执行。它将在事件循环的下一个循环中执行。如果我错了,请纠正我... .
  • 我有第二个疑问 => 在事件循环的 下一个周期 readFile() 回调 将在哪个阶段执行 待定 I/O 阶段或 I/O 轮询阶段。
【解决方案2】:

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

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

然后我认为事件循环将移动到计时器(无)、I/O 回调(fs.readFile 的回调)、空闲/准备(无)、轮询(无)、检查(setImmediate 的回调)最后关闭回调( none) 按顺序执行,但结果是 setImmediate() 仍然先运行。

问题在于事件循环的 I/O 回调阶段会运行已在事件队列中的 I/O 回调,但它们在完成后不会自动放入事件队列中。相反,它们仅在流程后期的I/O poll 步骤中被放入事件队列(见下图)。因此,第一次通过 I/O 回调阶段时,还没有要运行的 I/O 回调,因此您不会得到 readfile 的输出,而您认为应该会。

但是,setImmediate() 回调在第一次通过事件循环时已准备就绪,因此它可以在 readFile() 回调之前运行。

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

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

  1. 当 while 循环完成时,它会从计时器回调开始,并看到计时器已准备好运行,因此它会运行它。
  2. 然后,它会运行任何已经存在但还没有的 I/O 回调。来自readFile() 的 I/O 回调尚未收集。它将在此周期的后期收集。
  3. 然后,它会经过几个其他阶段并进行 I/O 轮询。有收集readFile() 回调事件并将其放入 I/O 队列(但尚未运行)。
  4. 然后,它进入 checkHandlers 阶段,在该阶段运行setImmediate() 回调。
  5. 然后,它再次启动事件循环。没有计时器,所以它进入 I/O 回调,最后找到并运行 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() 完成其工作(在后台运行)。它的事件已准备就绪,但尚未在相应的事件队列中(稍后会详细介绍)
  6. while 循环在 1 秒后循环结束,这个初始的 Javascript 序列完成并返回到系统
  7. 解释器现在需要从事件循环中获取“下一个”事件。但是,所有类型的事件都没有得到平等对待。事件系统有一个特定的顺序,它处理队列中不同类型的事件。在我们的例子中,这里首先处理定时器事件(我将在下文中解释这一点)。系统检查是否有任何计时器“过期”并准备好调用它们的回调。在这种情况下,它会发现我们的计时器已“过期”并准备就绪。
  8. 定时器回调被调用,我们看到控制台消息timer
  9. 没有更多的计时器,因此事件循环进入下一个阶段。事件循环的下一阶段是运行任何挂起的 I/O 回调。但是,事件队列中还没有挂起的 I/O 回调。尽管readFile() 现在已经完成,但它还没有在队列中(解释来了)。
  10. 然后,下一步是收集所有已完成的 I/O 事件并让它们准备好运行。在这里,readFile() 事件将被收集(虽然尚未运行)并放入 I/O 事件队列。
  11. 然后下一步是运行任何待处理的setImmediate() 处理程序。当它这样做时,我们会得到输出immediate
  12. 然后,事件过程的下一步是运行任何关闭处理程序(这里没有要运行的)。
  13. 然后,事件循环通过检查计时器重新开始。没有待运行的计时器。
  14. 然后,事件循环运行所有挂起的 I/O 回调。这里readFile() 回调运行,我们在控制台中看到readfile
  15. 程序没有更多的事件等待,所以它执行。

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

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

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

最初让我感到惊讶的是为什么readFile 总是出现在最后。这是因为即使readFile() 操作完成,它也不会立即放入队列中。相反,在事件循环中有一个步骤,其中收集已完成的 I/O 事件(将在事件循环的下一个循环中处理)并在 I/O 事件之前的当前循环结束时处理 setImmediate() 事件刚刚收集的。这使得 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 循环运行 1000 毫秒
  4. 代码完成执行并将控制权返回给系统
  5. 系统从以计时器事件开始的事件逻辑的顶部开始。我们之前启动的计时器现在已完成,因此它运行其回调并记录 timer
  6. 由于没有更多的计时器,事件循环会运行其他几种类型的事件队列,直到到达运行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

在上面的流程图中,您可以看到“运行已完成的 I/O 处理程序”步骤所在的位置。因为setTimeout()setImmediate() 调用将在I/O 处理程序中进行调度,所以它们将在事件循环的“运行已完成的I/O 处理程序”阶段进行调度。按照事件循环的流程,setImmediate() 将在事件循环返回到服务计时器之前在“检查处理程序”阶段得到服务。

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

【讨论】:

  • 您好 jfriend00,感谢您的解释。但是在我将一些printf放入libuv事件循环之后,我确定js readFile的回调是在uv__io_poll函数内部执行的。 ``` sss ```
  • 您好 jfriend00,感谢您的解释。但是我把一些printf放到libuv事件循环之后,我确定js readFile的回调是在uv__io_poll函数里面执行的,而不是在uv__run_pending里面。 'readfile' 出现在 'immediate' 之后的原因是 readFile 操作不能在一次 uv__io_poll 调用中完成。
  • @RueianOneecom - 我不确定你在说什么,但readFile() 肯定需要不止一步的事件循环才能完成,因为它将包含多个异步操作(打开文件,读取文件,关闭文件),这将在事件循环中安排多个事件。
  • 14. Then, the event loop runs any pending I/O callbacks. Here the readFile() callback runs and we see readfile in the console.。 @jfriend00 - 但是,在将一些 printf 调用注入 libuv 事件循环后,我确信 readFile() 回调在 IO Poll 阶段运行,而不是在 pending IO callbacks 阶段。
  • @RueianOneecom - 好的,数据很好。我认为我需要考虑的逻辑是 readFile() 本身实际上是一系列至少 3 个异步操作。我得看看我能不能把头重新考虑进去(第一次尝试遵循这一切让我的大脑受伤了)。
猜你喜欢
  • 1970-01-01
  • 2023-01-04
  • 1970-01-01
  • 2017-07-03
  • 1970-01-01
  • 1970-01-01
  • 2017-11-15
  • 2016-05-30
相关资源
最近更新 更多