定義の整理 ── 並行と並列は別物だ
並行処理(concurrent)と並列処理(parallel)は、混同されやすいですが本質的に異なります。
- 並行処理:1つの実行単位が複数のタスクを「交互に」こなします。同時には動きません。
- 並列処理:複数の実行単位が複数のタスクを「文字どおり同時に」動かします。
レストランのウェイターに例えると、1人のウェイターがテーブルを次々と回るのが並行、複数のウェイターが別々のテーブルを同時に担当するのが並列です。
JS の非同期 = 並行処理
JavaScript はシングルスレッドで動く言語です。すべての JS コードは 1 本のスレッド上で実行されます。これは「JS では並列処理ができない」を意味するのではなく、「通常の JS は並行処理である」を意味します。
その中心にあるのがイベントループです。
- コールスタックでコードを実行します
- スタックが空になります
- イベントキューに積まれたコールバックを取り出して実行します
- また 2 に戻ります
setTimeout・fetch・addEventListener などはすべてこの仕組みに乗っています。「非同期」と呼ばれますが、スレッドが切り替わるわけではなく、スタックが空になった瞬間に次のコールバックが走るだけです。
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 や構文強調のパース処理
逆に、fetch や setTimeout などの 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.all と Worker を使い分ける判断軸はシンプルです。待ち時間を重ねたいだけなら並行で十分、計算を並列に動かしたいなら Worker を使います。この 1 文を押さえておけば、ほとんどの場面で迷いません。
非同期通信(fetch・Ajax)の基礎については「Ajax から fetch へ ── JavaScript 非同期通信の地図を描く」を参照してください。