OP的原始问题
在forEach循环中使用async/await有什么问题吗。。。
在@Bergi选择的答案中,它展示了如何串行和并行处理。然而,并行性还存在其他问题-
订单--@chharvey注意到-
例如,如果一个非常小的文件在一个非常大的文件之前完成了读取,那么它将首先被记录,即使小文件在文件数组中位于大文件之后。
可能一次打开太多文件--Bergi在另一个答案下的评论
同时打开数千个文件以同时读取它们也是不好的。人们总是要评估顺序、并行或混合方法是否更好。
因此,让我们来解决这些问题,展示实际的代码,简洁明了,不使用第三方库。易于剪切、粘贴和修改的东西。
并行读取(一次读取),串行打印(每个文件尽可能早)。
最简单的改进是像@Bergi的回答那样执行完全并行,但做了一个小改动,以便在保持顺序的同时尽快打印每个文件。
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个。就像一家一次只允许这么多顾客进入的商店(至少在新冠疫情期间)。
首先引入了一个helper函数-
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返回几个财产
承诺类型承诺引导类型函数()=>void
承诺有两个阶段
承诺开始一项任务作为一个承诺,完成一项已经开始的任务。
当调用boot()时,promise从第一状态转换到第二状态。
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:用于打印
现在的区别是不允许并发运行超过concurrentLimit Promise。
重要的变量是
boots:要调用以强制其相应Promise转换的函数数组。它仅在分支1中使用。set:在随机访问容器中有Promise,这样一旦实现,就可以很容易地删除它们。此容器仅在分支1中使用。bootableProms:这些是与最初在集合中的Promise相同的Promise,但它是一个数组而不是集合,并且该数组从未更改。它仅在分支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
可在typescript游乐场沙盒中执行