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

Posted

技术标签:

【中文标题】使用 IO 操作时如何不让任务过度饱和 libuv【英文标题】:How to not oversaturate libuv with tasks when using IO operations 【发布时间】:2021-04-26 09:04:45 【问题描述】:

我正在使用 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 请求过饱和?

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

【问题讨论】:

【参考方案1】:

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

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

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

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

鉴于您的 sn-p 和约束,如果最大深度为 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

【讨论】:

谢谢!这是一个了不起的见解!我会查看您的链接,所以如果我理解正确,libuv 将自行处理宏任务,因此如果我在libuv 推送数十亿次async readFile(..) 操作,则无需担心,因为它只会限制真正的并行通过内部限制执行并分批执行。我总结得对吗? 我想你做到了。我自己对迄今为止遇到的所有材料的理解和知识表明了这一点。

以上是关于使用 IO 操作时如何不让任务过度饱和 libuv的主要内容,如果未能解决你的问题,请参考以下文章

node.js 系列 7 异步IO

libuv之三:文件系统

从libuv源码学习线程池

从libuv源码学习线程池

libuv 1.0.0 发布,Node.js 的网络 IO 库扩展

libuv vs asyncio (python)