定義の整理 ── 並行と並列は別物だ

並行処理(concurrent)と並列処理(parallel)は、混同されやすいですが本質的に異なります。

  • 並行処理:1つの実行単位が複数のタスクを「交互に」こなします。同時には動きません。
  • 並列処理:複数の実行単位が複数のタスクを「文字どおり同時に」動かします。

レストランのウェイターに例えると、1人のウェイターがテーブルを次々と回るのが並行、複数のウェイターが別々のテーブルを同時に担当するのが並列です。

並行処理と並列処理の違い

JS の非同期 = 並行処理

JavaScript はシングルスレッドで動く言語です。すべての JS コードは 1 本のスレッド上で実行されます。これは「JS では並列処理ができない」を意味するのではなく、「通常の JS は並行処理である」を意味します。

その中心にあるのがイベントループです。

  1. コールスタックでコードを実行します
  2. スタックが空になります
  3. イベントキューに積まれたコールバックを取り出して実行します
  4. また 2 に戻ります

setTimeoutfetchaddEventListener などはすべてこの仕組みに乗っています。「非同期」と呼ばれますが、スレッドが切り替わるわけではなく、スタックが空になった瞬間に次のコールバックが走るだけです。

1 つのコールバックが実行中は他のイベントは待たされます。これが「JS は並行だが並列ではない」という意味の実態です。

「並列っぽく見える」Promise.all

Promise.all を使うと、複数の非同期処理を「同時に」走らせているように見えます。

const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);

しかし、これも並行処理です。3 つの fetch リクエストはほぼ同時に発行されますが、それぞれのレスポンス処理は 1 本のスレッドが順番にさばきます。I/O 待ち(ネットワーク通信やファイル読み込み)は OS やブラウザに委ねられており、その間 JS スレッドは別の処理を進めます。

I/O バウンドなタスクであれば、Promise.all は十分に効率的です。
スレッドが何かを「待っている」時間をゼロに近づけられます。

問題になるのは CPU バウンドなタスク、つまり計算量が多く JS スレッドを長時間占有するケースです。

真の並列:Web Worker

CPU バウンドなタスクをメインスレッドで実行すると、UI の描画やユーザー操作への反応がブロックされます。スクロールが止まり、アニメーションがカクつきます。

これを解決するのが Web Worker です。Worker はメインスレッドとは別のスレッドでスクリプトを実行します。これが JavaScript における真の並列処理になります。

// worker.js(別ファイル)
self.onmessage = function (e) {
  const result = heavyCalc(e.data); // 重い計算
  self.postMessage(result);
};

// main.js
const worker = new Worker('worker.js');

worker.postMessage(inputData);

worker.onmessage = function (e) {
  console.log('結果:', e.data);
};

メインスレッドと Worker はメッセージパッシングで通信します。共有メモリは持ちません(SharedArrayBuffer を使う高度なケースを除く)。Worker 内では DOM にアクセスできません。この制約によってスレッド競合のリスクを排除しています。

Web Worker の使い所

Worker が有効なのはCPU バウンドなタスクです。具体的には以下のようなケースが挙げられます。

  • 大量データの集計・フィルタリング・ソート
  • 画像処理(ピクセル操作、フィルター適用)
  • 暗号化・ハッシュ計算
  • WebAssembly を使った重い数値計算
  • Markdown や構文強調のパース処理

逆に、fetchsetTimeout などの I/O バウンドなタスクは、Worker に移しても速くはなりません。イベントループで十分にさばけるためです。

Worker のオーバーヘッド(起動コスト・メッセージのシリアライズ)も無視できません。軽い処理を Worker に投げると、かえって遅くなることがあります。判断基準は「メインスレッドがブロックされているか」です。

まとめ

観点 Promise / async-await Web Worker
スレッド数 1(シングルスレッド) 2以上(マルチスレッド)
処理モデル 並行(concurrent) 並列(parallel)
向いているタスク I/O バウンド CPU バウンド
DOM へのアクセス 不可
導入コスト 低い やや高い(ファイル分割・メッセージ設計)

JavaScript の非同期は「並行処理」であり、スレッドは 1 本のままです。await で見た目はスッキリしても、CPU を長時間使う処理はメインスレッドをブロックします。そのような処理には Web Worker を使い、メインスレッドを UI の応答に集中させましょう。

Promise.allWorker を使い分ける判断軸はシンプルです。待ち時間を重ねたいだけなら並行で十分、計算を並列に動かしたいなら Worker を使います。この 1 文を押さえておけば、ほとんどの場面で迷いません。

非同期通信(fetch・Ajax)の基礎については「Ajax から fetch へ ── JavaScript 非同期通信の地図を描く」を参照してください。