非同期IOであるJavaScriptの非同期処理を扱いやすくするためにPromiseやasync/awaitが使われることが標準となってきました。
非同期処理を複数同時に実行する場合、Promise.all
を利用することで同時並行で進め、全てが完了したタイミングで処理が終了してくれます。
多くの場合はPromise.all
で事足りるのですが、これが数百・数千といった単位になった場合、一気に外部サイトにリクエストを送ろうものなら大きな問題となりうるので、並列の実行数を規定したい時の対応を紹介します。
外部ライブラリを使わなくても簡単に実装できる
Promise.all
の並列数を設定したいニーズは一定数あるようで、有力なPromiseライブラリであるbluebirdにもPromise.mapという機能が用意されているので、そちらを利用すると指定した並列数で処理を実行してくれます。
とはいえ並列数を指定するためだけに比較的大きなライブラリであるbluebirdをインストールするのも気が引けるので、どうせならもっと簡単に実現できるのではないかと考えて実装したところ、TypeScriptでわずか15行で実現できました。
const concurrentPromise = async <T>(promises: (() => Promise<T>)[], concurrency: number): Promise<T[]> => {
const results: T[] = [];
let currentIndex = 0;
while (true) {
const chunks = promises.slice(currentIndex, currentIndex + concurrency);
if (chunks.length === 0) {
break;
}
Array.prototype.push.apply(results, await Promise.all(chunks.map(c => c())));
currentIndex += concurrency;
}
return results;
};
処理の内容としては並列数分だけ先頭から要素を取り出し、要素の末尾までループで実行し続け、最後まで完了した時点で実行結果を返します。
実行結果の確認
それでは上の関数を使って実際に動かしてみましょう。setTimeoutするだけの関数を配列に入れて実行します。
const timeoutExecution = (time: number): Promise<number> => {
return new Promise(resolve => {
setTimeout(() => {
console.log(time);
resolve(time);
}, time);
});
};
(async () => {
const promises = [...Array(10).fill(null)].map((_, i) => timeoutExecution.bind(null, i * 100));
await concurrentPromise(promises, 5);
})();
実行結果は以下の通りです。
0
100
200
300
400
500
600
700
800
900
文字だけを見ると伝わりませんが、確かに並列処理がされており、一つの並列セットの処理が完了してから次のセットが実行されていました。
引数を取る関数を渡す場合
型定義を見ると引数を取らないPromise型の関数のみ渡せるように見えが、引数を取ることも可能です。
const sum = (x: number, y: number): number => {
return x + y;
};
const executor = sum.bind(null, 10, 20);
console.log(executor()); // --> 30
このようにbind
を通じて引数を設定した変数に代入すれば、引数なしのFunction型に変換できます。
かつてはbind
を使うとTypeScriptの型情報を失ってしまっていたのですが、Version 3.2からbindの型チェックが可能となり、tsconfig.jsonに"strictBindCallApply": true
を追加すれば型チェックを行ってくれます。(ただしnullableのチェックはまだ対応していないようです)
何よりコード量が少なく、かつある程度汎用的に利用できるので、同様のユースケースの際にぜひ参考にしてみてください。