Node.js中Timer阶段与I/O阶段执行顺序的思考

101 阅读2分钟

本文将通过两个对比实验来探讨这一主题,并分析执行顺序的不确定性及其影响因素。

实验1: 无同步任务的情况下,Timer与I/O的执行顺序

const fs = require("fs");

setTimeout(() => {
  console.log("setTimeout");
}, 0);

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

执行结果

实验的执行结果显示,setTimeoutreadFile的执行顺序并不固定。

Snipaste_2024-11-19_17-25-01.jpg

原因探讨

经查阅 chromium V8 中 DOMTimer.cpp源码发现: setTimeout(fn, 0)实际上会被Node.js设置为setTimeout(fn, 1)

Snipaste_2024-11-19_17-26-46.jpg

进一步解释

V8引擎写了 setTimeout如果是0ms 最小按照1ms来执行,Nodejs依然需要计算1ms是否已经过去, 分为2种情况:

  1. 如果cpu不繁忙,事件循环在0.05ms进入Timer阶段,Timer的回调函数不会立即回到队列,此时控制权将移交给I/O queue,因此优先输出readFile;在下一次事件循环中,Nodejs发现1ms已经过去,继续执行setTimeout;
  2. 如果cpu繁忙,事件循环在1.05ms进入Timer阶段,此时1ms已经过去,Timer的回调函数已经在Timer queue了,因此优先输出setTimeout,随后输出readFile。

影响因素

影响Timer和I/O执行顺序的因素包括:

  1. 进程性能:Node.js进程的当前负载会影响事件循环的执行速度,进而影响Timer和I/O的执行顺序。
  2. 其他应用程序:当前机器上其他正在运行的应用程序可能会占用CPU和I/O资源,从而影响Node.js进程的性能。
  3. Node.js事件循环本身:事件循环的内部机制,包括不同阶段的执行顺序和调度策略,也会影响Timer和I/O的执行顺序。

实验2: 有耗时同步任务的情况下,Timer与I/O的执行顺序

const fs = require("fs");

setTimeout(() => {
  console.log("setTimeout");
}, 0);

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

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

执行结果

实验的执行结果显示,setTimeoutreadFile的执行顺序是固定的。

Snipaste_2024-11-19_17-39-12.jpg

进一步解释

加了同步任务后,根据node的事件循环机制,优先执行用户输入的代码(即同步任务),执行完毕后,此时进入Timer阶段已经过去了1ms,Timer的回调函数已经在Timer queue了,因此优先输出setTimeout,随后输出readFile。这是确定的,