🐈

[JavaScript] forEach内の非同期処理をセマフォで同期させる

2022/07/19に公開

前回

前回は Promise の then を数珠繋ぎにして逐次処理を可能としました。今回は最大並列実行数を管理できるようにセマフォを実装したいと思います。ただ、セマフォ相当の動作はライブラリとして公開されているものもあるので、差別化を図るため今回は世界最小のコード量でセマフォを実装してみます。

https://zenn.dev/sora_kumo/articles/612ca66c68ff52

実装

ということで作りました。

コード量削減と終了待ち用の Promise.all 相当の機能を入れているので、ピタゴラスイッチ的なソースになっています。

JavaScript の並列処理はマルチスレッドではないので、実際にやっているのはスケジュール制御用の配列に関数を積んで、並列最大数に沿って実行をかけています。

const semaphore = (
  limit = 1,
  count = 0,
  rs = new Array<() => void>(),
  all?: () => void
) => ({
  acquire: () =>
    ++count > limit && new Promise<void>((resolve) => rs.push(resolve)),
  release: () => (--count ? rs.shift()?.() : all?.()),
  all: () => count && new Promise<void>((resolve) => (all = resolve)),
});

テスト用非同期共通関数

実行時間と引数の内容を表示した後、1 秒待機します。

const f = (value: string) =>
  new Promise<void>((resolve) => {
    console.timeLog("debug", value);
    setTimeout(resolve, 1000);
  });

並列 1 のソースとその実行結果

const main = async () => {
  console.time("debug");
  const s = semaphore();
  ["A", "B", "C", "D", "E"].forEach(async (v) => {
    await s.acquire();
    await f(v);
    s.release();
  });
  await s.all(); //全ての終了を待つ
  console.timeLog("debug", "終了");
};
main();

おおよそ一秒間隔で実行されているのが確認出来ます。
並列 1 なら逐次処理相当になります。

debug: 0.197ms A
debug: 1.014s B
debug: 2.027s C
debug: 3.039s D
debug: 4.040s E
debug: 5.050s 終了

並列 2 のソースとその実行結果

const main = async () => {
  console.time("debug");
  const s = semaphore(2);
  ["A", "B", "C", "D", "E"].forEach(async (v) => {
    await s.acquire();
    await f(v);
    s.release();
  });
  await s.all(); //全ての終了を待つ
  console.timeLog("debug", "終了");
};
main();

B の数値が大きく見えますが、単位が ms なので注意してください。
AB がほぼ 0 秒、CD が 1 秒、E が 2 秒、終了が 3 秒後に実行されているのが確認出来ます。

debug: 0.19ms A
debug: 1.826ms B
debug: 1.005s C
debug: 1.005s D
debug: 2.012s E
debug: 3.028s 終了

まとめ

前回も述べているとおり forEach 自体は非同期処理ではありません。また async/await が使えないわけでもありません。同期させるロジックを組めばその通り動きます。やろうと思えば大したコード量にもならずサクッと書くことが可能なのです。

GitHubで編集を提案

Discussion