LoginSignup
32
24

More than 1 year has passed since last update.

Promise.allの並列処理数を規定する関数を15行でサクッと実装する

Last updated at Posted at 2019-05-07

非同期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のチェックはまだ対応していないようです)

何よりコード量が少なく、かつある程度汎用的に利用できるので、同様のユースケースの際にぜひ参考にしてみてください。

32
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
24