使用 IO 操作时如何不让任务使 libuv 过饱和

问题描述

我正在使用 Typescript,因此使用 libuv 来执行任何 IO 操作。在我的特定场景中,我正在获取给定文件的指纹哈希。为了在这里说明我的问题,请考虑输入文件1TB文件。要获取文件的指纹,我可以通过文件流打开文件并更新哈希:

return new Promise((resolve,reject) => {
    const hash = crypto.createHash('sha256');
    const fh = fse.createReadStream(filepath,{
        highWaterMark : 100000000
    });

    fh.on('data',(d) => { hash.update(d); });
    fh.on('end',() => {
        resolve(hash);
    });
    fh.on('error',reject);
});

鉴于它是顺序方法,上面的示例相当慢。所以我想到的一种更快的方法是将计算分成 N 个块,如下所示:

let promises = [];
for (let i = 0; i < N; ++i) {
    promises.push(calculateFilePart(file,from,to));
}
return Promise.all(all);

鉴于上面的例子,假设 N 是 1000000,这是否意味着 libuv后台同时启动 1000000 个异步 I/O 操作?还是 libuv 会自动将它们分批排队以避免 IO 请求过饱和?

非常感谢您对本主题的任何帮助!

解决方法

我将尝试尽可能简短地总结一些关键概念。我会在下面留下链接以供参考,以便您验证事实。

Promise 将一个任务添加到一个叫做 Microtask Queue 的东西中。在事件循环的每次迭代中,当调用堆栈为空时,会处理来自微任务队列的任务。这称为 tick。所以,每一个tick,都会处理一些来自Microtask Queue的任务。

对于每个进程刻度,都有一个最大深度 (process.maxTickDepth)。这指定了要从 Microtask Queue 中卸载并推送到调用堆栈中的任务数。

算法的主要部分涉及读取 I/O 操作的内容。此类操作被推送到称为宏任务队列的单独队列中。当一个调度宏任务操作完成并且它具有指定的内容块时,读取操作的事件处理程序将排队进入微任务队列以在下一个滴答时处理。

给定您的代码段和约束,如果最大深度为 1000,那么您的算法要完全更新散列,至少需要传递 N / 1000 = 1000000 / 1000 = 1000 个刻度。这意味着 Node.js 进程每次只会处理特定数量的任务。

我希望这为您提供了您正在寻找的理解。

参考文献:

Node.js Under The Hood #3 - Deep Dive Into the Event Loop

MDN Documentation on Promise.all