我刚刚学习 Node 中的事件循环如何工作的细节。
我了解到 Promise 队列的优先级高于计时器队列,后者的优先级高于 I/O 队列。
async function asyncFunc() {
let sum = 0;
for (let i = 0; i < 100000; i++) {
sum += i;
}
}
async function main() {
let start = Date.now();
let interval = setInterval(() => {
let time = Date.now() - start;
console.log(time);
}, 1000);
while (true) {
await asyncFunc();
}
}
main();
此代码从不记录时间,因为承诺队列永远不会清除。
import fs from "fs-extra";
async function asyncFunc(j) {
let sum = 0;
console.log("checkpoint 1");
await fs.copy("file.txt", `file${j}.txt`, { overwrite: false });
for (let i = 0; i < 1000000000; i++) {
sum += i;
}
console.log("checkpoint 2");
}
async function main() {
let start = Date.now();
let interval = setInterval(() => {
let time = Date.now() - start;
console.log(time);
}, 1000);
while (true) {
const promises = [];
for (let j = 0; j < 4; j++) {
promises.push(asyncFunc(j));
}
await Promise.all(promises);
}
}
main();
为什么会输出这段代码
checkpoint 1
checkpoint 1
checkpoint 1
checkpoint 1
checkpoint 2
checkpoint 2
checkpoint 2
2123
checkpoint 2
?
在 cpu 密集型工作开始之前,所有副本都会在空文件上调用,并且它们应该花费很少的时间。
如果文件复制完成时向 Promise 队列返回回调,我认为永远不应该记录时间,因为 Promise 队列在 cpu 密集的工作完成时始终会有一个回调在等待。
如果它向 I/O 队列返回回调,我认为时间日志将在最后一个检查点 1 之后发生,因为承诺队列将为空并且 I/O 队列将有四个回调。
这是怎么回事?这只是一个奇怪的竞争条件吗?
定时器队列中的作业比 I/O 队列中的作业优先级的原因在文章 Node.js 事件循环 (nodejs.org) 中进行了解释,其中相关的两个阶段被命名为“定时器” ”和“轮询”(针对 I/O):
为了防止轮询阶段使事件循环处于饥饿状态,libuv(实现 Node.js 事件循环和平台所有异步行为的 C 库)在停止轮询更多内容之前也有一个硬性最大值(取决于系统)事件。
再次:
如果轮询队列不为空,事件循环将迭代其回调队列,同步执行它们,直到队列耗尽或达到系统相关的硬限制。
这显然是您的场景中发生的情况:执行了三个繁忙循环后,事件循环认为轮询阶段已经足够长了,并进入循环的下一个阶段,暂时放弃非空 I /O 队列。
因此计时器回调有机会执行,之后循环返回轮询阶段以继续处理该队列中的剩余作业。