OPの元の質問
forEach ループで async/await を使用する際に問題はありますか? ...
@Bergi の選択された回答である程度カバーされ、シリアルおよびパラレルで処理する方法が示されました。ただし、並列処理には他にも問題があります -
- 注文 -- @chharveyは次のことに注意してください --
たとえば、非常に大きなファイルの前に非常に小さなファイルの読み取りが終了した場合、小さなファイルが files 配列内の大きなファイルの後に来る場合でも、最初にログに記録されます。
- 一度にあまりにも多くのファイルを開く可能性があります - 別の回答の下での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);
})(),
]);
}
上記では、2 つの別々のブランチが同時に実行されています。
- ブランチ 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 };
}
funcitonは、タスクを開始するための引数としてbootablePromise(kickMe:() => Promise<any>)
関数を取りkickMe
ます (この場合はreadFile
)。しかし、すぐに開始されるわけではありません。
bootablePromise
いくつかのプロパティを返します
promise
タイプのPromise
boot
型関数の()=>void
promise
人生には2つの段階がある
- タスクを開始するという約束であること
- すでに開始されているタスクを完了する約束であること。
promise
boot()
が呼び出されると、最初の状態から 2 番目の状態に遷移します。
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);
})(),
]);
}
前と同じように、2 つのブランチがあります。
- ブランチ 1: 同時実行の実行と処理用。
- ブランチ 2: 印刷用
現在の違いは、concurLimit
promise を同時に実行することは許可されていないことです。
重要な変数は次のとおりです。
boots
: 対応する遷移の約束を強制するために呼び出す関数の配列。ブランチ 1 でのみ使用されます。
set
: ランダム アクセス コンテナーには promise が含まれているため、満たされた後は簡単に削除できます。このコンテナはブランチ 1 でのみ使用されます。
bootableProms
: これらは の最初のとおりの小さな約束ですが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
typescript プレイグラウンド サンドボックスで実行可能ファイルとして利用可能