テーマの整理:「並列処理」とは何か
ここでいう「並列処理」は、配列の要素に対する非同期処理を「同時に複数まとめて動かす」ことです。 順次処理(1 件ずつ)に比べて速く終わりやすい一方で、負荷やレート制限を意識しないと危険にもなります。
業務だと、こんな場面で並列処理が欲しくなります。
複数ユーザーの情報を一気に取得したい。 複数ファイルを同時にアップロードしたい。 複数のチェック処理をまとめて投げて、全部終わってから結果を見たい。
ここでは、「配列 × 非同期 × 並列」を扱うためのユーティリティを、初心者向けにかみ砕いて説明します。
基本形:完全並列で動かす parallelMap(Promise.all)
実装と考え方
まずは「全部並列で動かして、全部終わったら結果を配列で返す」一番シンプルな形です。 これは、Promise.all をラップした「非同期 map の並列版」と考えてください。
async function parallelMap(array, asyncMapper) {
if (!Array.isArray(array)) {
return [];
}
if (typeof asyncMapper !== "function") {
return array.slice();
}
const promises = array.map((item, index) => asyncMapper(item, index));
return Promise.all(promises);
}
JavaScript重要なポイントをかみ砕きます。
array は「処理したい対象の配列」。 asyncMapper は「各要素を非同期で変換する関数」(async 関数 or Promise を返す関数)。 まず普通の map で「Promise の配列」を作る。 Promise.all で「全部の Promise が終わるのを待って、結果を配列で受け取る」。
つまり、「同期の map を、そのまま並列の async にしたもの」です。
例題:ユーザー ID の配列から、API でユーザー情報を並列取得する
async function fetchUser(id) {
console.log("start", id);
await new Promise((r) => setTimeout(r, 500));
console.log("end", id);
return { id, name: `User-${id}` };
}
async function main() {
const ids = [1, 2, 3];
const users = await parallelMap(ids, (id) => fetchUser(id));
console.log(users);
}
main();
JavaScriptログを見ると、「1・2・3 がほぼ同時に start して、ほぼ同時に end している」ことが分かるはずです。 これが「完全並列」のイメージです。
完全並列の注意点:速いけど“雑に速い”
完全並列は速いですが、業務では次のようなリスクがあります。
API のレート制限を一気に踏み抜く可能性がある。 外部サービスに負荷をかけすぎて、エラーが増える。 大量のファイルアップロードなどでネットワーク帯域を食いつぶす。
「並列にできるからといって、何でも全部並列にしていいわけではない」というのが、実務での大事な感覚です。 そこで出てくるのが、「同時実行数を制限した並列処理」です。
同時実行数を制限する parallelLimit(制限付き並列)
実装と考え方
「最大 N 個まで並列で動かして、それ以上は順番待ちにする」ユーティリティを作ります。 これは、前に説明した runWithLimit の「map 版」と考えてください。
async function parallelLimit(array, asyncMapper, limit = 5) {
if (!Array.isArray(array)) {
return [];
}
if (typeof asyncMapper !== "function") {
return array.slice();
}
if (typeof limit !== "number" || limit <= 0) {
limit = 1;
}
const result = new Array(array.length);
let currentIndex = 0;
async function worker() {
while (currentIndex < array.length) {
const index = currentIndex;
currentIndex += 1;
const item = array[index];
const value = await asyncMapper(item, index);
result[index] = value;
}
}
const workers = [];
const workerCount = Math.min(limit, array.length);
for (let i = 0; i < workerCount; i++) {
workers.push(worker());
}
await Promise.all(workers);
return result;
}
JavaScript重要なポイントを丁寧に分解します。
limit は「同時に動かす最大数」。 result は「最終的な結果を入れる配列」で、元の配列と同じ長さ。 currentIndex は「次に処理すべき要素の位置」を共有するカウンタ。 worker は「仕事を取りに行って処理する役割」を持つ async 関数。 worker を limit 個だけ立ち上げることで、「同時に動く処理の数」を制限する。 全部の worker が終わったら、result に全要素の結果が入っている。
これが「制限付き並列処理」の基本パターンです。
例題:ユーザー ID を最大 2 並列で API 取得する
async function fetchUser(id) {
console.log("start", id);
await new Promise((r) => setTimeout(r, 500));
console.log("end", id);
return { id, name: `User-${id}` };
}
async function main() {
const ids = [1, 2, 3, 4];
const users = await parallelLimit(ids, (id) => fetchUser(id), 2);
console.log(users);
}
main();
JavaScriptログを眺めると、「常に 2 つまでしか同時に start していない」ことが分かるはずです。 これが、レート制限や負荷を意識した“業務レベルの並列処理”です。
並列処理+エラー制御:失敗をどう扱うか
全部成功しないとダメなケース
「1 つでも失敗したら全体を失敗にしたい」なら、parallelMap の中で Promise.all をそのまま使う形で十分です。 この場合、どれか 1 つが reject すると、全体が reject します。
async function parallelMapStrict(array, asyncMapper) {
const promises = array.map((item, index) => asyncMapper(item, index));
return Promise.all(promises); // 1 つでも失敗したら全体が失敗
}
JavaScript一部失敗は許容したいケース
「失敗したものは失敗として記録しつつ、成功したものはちゃんと使いたい」なら、 Promise.allSettled を使うか、parallelLimit の中で try/catch する形が向いています。
例えば、制限付き並列で「成功なら値、失敗ならエラーオブジェクト」を結果に入れる形です。
async function parallelLimitSafe(array, asyncMapper, limit = 5) {
if (!Array.isArray(array)) return [];
if (typeof asyncMapper !== "function") return array.slice();
if (typeof limit !== "number" || limit <= 0) limit = 1;
const result = new Array(array.length);
let currentIndex = 0;
async function worker() {
while (currentIndex < array.length) {
const index = currentIndex;
currentIndex += 1;
const item = array[index];
try {
result[index] = await asyncMapper(item, index);
} catch (e) {
result[index] = e; // 失敗はエラーとして記録
}
}
}
const workers = [];
const workerCount = Math.min(limit, array.length);
for (let i = 0; i < workerCount; i++) {
workers.push(worker());
}
await Promise.all(workers);
return result;
}
JavaScript業務では、「一部失敗は許容するが、ログには残したい」「後で再試行したい」といった要件が多いので、 「失敗をどう結果に残すか」まで含めて並列処理ユーティリティを設計しておくと、後々楽になります。
並列処理を選ぶときに必ず考えてほしいこと
本当に並列にしていいか?
並列処理は速くなりがちですが、 「外部サービスの都合」「レート制限」「ネットワーク負荷」「DB ロック」など、 現実世界の制約を無視すると簡単に事故ります。
そこで、毎回自分にこう問いかけてほしいです。
この処理は、順番にやる必要があるか? 外部サービスに負荷をかけすぎないか? 同時に何件までなら安全か?
その答えを、parallelMap と parallelLimit のどちらを使うか、limit をいくつにするか、という形に落とし込んでいきます。
並列処理は「速さ」と「安全さ」のバランス調整
完全並列は「速さ寄り」。 順次処理は「安全さ寄り」。 制限付き並列は「その間を取る」ものです。
業務コードでは、このバランスを意識して設計することがとても大事です。 「とりあえず全部並列」は、最初は気持ちいいですが、後で痛い目を見ることが多いです。
手を動かして「並列処理」の感覚をつかむ
次のコードを実行して、完全並列と制限付き並列の違いをログで体感してみてください。
async function parallelMap(array, asyncMapper) {
if (!Array.isArray(array)) return [];
if (typeof asyncMapper !== "function") return array.slice();
const promises = array.map((item, index) => asyncMapper(item, index));
return Promise.all(promises);
}
async function parallelLimit(array, asyncMapper, limit = 2) {
if (!Array.isArray(array)) return [];
if (typeof asyncMapper !== "function") return array.slice();
if (typeof limit !== "number" || limit <= 0) limit = 1;
const result = new Array(array.length);
let currentIndex = 0;
async function worker() {
while (currentIndex < array.length) {
const index = currentIndex;
currentIndex += 1;
const item = array[index];
const value = await asyncMapper(item, index);
result[index] = value;
}
}
const workers = [];
const workerCount = Math.min(limit, array.length);
for (let i = 0; i < workerCount; i++) {
workers.push(worker());
}
await Promise.all(workers);
return result;
}
async function demo() {
const ids = [1, 2, 3, 4];
console.log("=== parallelMap (full parallel) ===");
await parallelMap(ids, async (id) => {
console.log("start", id);
await new Promise((r) => setTimeout(r, 500));
console.log("end", id);
return id * 10;
});
console.log("=== parallelLimit (limit=2) ===");
await parallelLimit(ids, async (id) => {
console.log("start", id);
await new Promise((r) => setTimeout(r, 500));
console.log("end", id);
return id * 10;
}, 2);
}
demo();
JavaScript「全部並列」と「最大 2 並列」の違いが、ログの出方でかなりはっきり見えるはずです。
まとめ:並列処理ユーティリティで「速さをコントロールする」
配列ユーティリティとしての「並列処理」は、 単に速くするためのテクニックではなく、「速さをちゃんとコントロールするための設計」です。
プロジェクトに例えば次のような関数を置いておくイメージです。
export async function parallelMap(...) { ... } // 完全並列
export async function parallelLimit(...) { ... } // 制限付き並列
JavaScriptそして、「配列 × async を速く回したいときは、必ずどちらかを選ぶ」と決めておく。 それだけで、非同期コードの挙動が読みやすくなり、 「どこまで並列にしてよくて、どこからは抑えるべきか」をチームで共有しやすくなります。

