OP的原始问题
在 forEach 循环中使用 async/await 有什么问题吗?...
@Bergi's selected answer在一定程度上涵盖了该答案,该答案显示了如何串行和并行处理。然而,并行性还有其他问题 -
- 订单—— @chharvey指出——
例如,如果一个非常小的文件在一个非常大的文件之前完成读取,它将首先被记录,即使文件数组中小文件在大文件之后也是如此。
- 可能一次打开太多文件——Bergi 在另一个答案下的评论
一次打开数千个文件以同时读取它们也不好。人们总是需要评估顺序、并行或混合方法是否更好。
因此,让我们解决这些问题,展示简洁明了的实际代码,并且不使用第三方库。可以轻松剪切、粘贴和修改的东西。
并行读取(一次全部),串行打印(每个文件尽可能早)。
最简单的改进是在@Bergi 的回答中执行完全并行,但做一个小改动,以便在保留 order 的同时尽快打印每个文件。
async function printFiles2() {
const readProms = (await getFilePaths()).map((file) =>
fs.readFile(file, "utf8")
);
await Promise.all([
await Promise.all(readProms), // branch 1
(async () => { // branch 2
for (const p of readProms) console.log(await p);
})(),
]);
}
上面,两个单独的分支同时运行。
- 分支 1:一次并行读取,
- 分支 2:串行读取以强制排序,但无需等待
那很简单。
在并发限制的情况下并行读取,串行打印(每个文件尽可能早)。
“并发限制”意味着不能同时N
读取多个文件。
就像一家商店一次只允许这么多顾客进来(至少在 COVID 期间)。
首先介绍一个辅助函数——
function bootablePromise(kickMe: () => Promise<any>) {
let resolve: (value: unknown) => void = () => {};
const promise = new Promise((res) => { resolve = res; });
const boot = () => { resolve(kickMe()); };
return { promise, boot };
}
函数bootablePromise(kickMe:() => Promise<any>)
将函数kickMe
作为参数来启动任务(在我们的例子中readFile
)。但它不会立即启动。
bootablePromise
返回几个属性
promise
类型Promise
boot
类型函数()=>void
promise
人生有两个阶段
- 承诺开始一项任务
- 作为一个承诺完成它已经开始的任务。
promise
boot()
调用时从第一个状态转换到第二个状态。
bootablePromise
用于printFiles
——
async function printFiles4() {
const files = await getFilePaths();
const boots: (() => void)[] = [];
const set: Set<Promise<{ pidx: number }>> = new Set<Promise<any>>();
const bootableProms = files.map((file,pidx) => {
const { promise, boot } = bootablePromise(() => fs.readFile(file, "utf8"));
boots.push(boot);
set.add(promise.then(() => ({ pidx })));
return promise;
});
const concurLimit = 2;
await Promise.all([
(async () => { // branch 1
let idx = 0;
boots.slice(0, concurLimit).forEach((b) => { b(); idx++; });
while (idx<boots.length) {
const { pidx } = await Promise.race([...set]);
set.delete([...set][pidx]);
boots[idx++]();
}
})(),
(async () => { // branch 2
for (const p of bootableProms) console.log(await p);
})(),
]);
}
和以前一样有两个分支
- 分支 1:用于运行和处理并发性。
- 分支 2:用于打印
concurLimit
现在的不同之处在于允许同时运行的承诺不超过承诺。
重要的变量是
boots
:要调用的函数数组以强制其转换相应的承诺。它仅用于分支 1。
set
:随机访问容器中有承诺,因此一旦履行就可以轻松删除它们。此 contianer 仅在分支 1 中使用。
bootableProms
:这些是最初的 smae 承诺set
,但它是一个数组而不是一个集合,并且该数组永远不会改变。它仅用于分支 2。
fs.readFile
使用需要以下时间的模拟运行(文件名与以毫秒为单位的时间)。
const timeTable = {
"1": 600,
"2": 500,
"3": 400,
"4": 300,
"5": 200,
"6": 100,
};
可以看到像这样的测试运行时间,表明并发正在工作——
[1]0--0.601
[2]0--0.502
[3]0.503--0.904
[4]0.608--0.908
[5]0.905--1.105
[6]0.905--1.005
在打字稿游乐场沙箱中作为可执行文件提供