はじめに:「並列 LINQ」は“同じ処理をみんなで手分けする”仕組み
同じような処理を大量のデータに対して繰り返すとき、
1 件ずつ順番に処理するとどうしても時間がかかります。
そこで出てくるのが「並列 LINQ(PLINQ)」です。
ざっくり言うと、
同じ処理を、複数のスレッドで同時に実行して、
トータルの処理時間を短くしよう
という仕組みです。
ここでは、初心者向けに
並列 LINQ の基本(AsParallel)
順次 LINQ との違い
注意すべきポイント(順序・副作用・重い処理かどうか)
業務での「ここなら使っていい」具体例
を、例題付きでかみ砕いて説明していきます。
並列 LINQ の基本:AsParallel で「並列モード」に切り替える
まずは普通の LINQ と見比べる
普通の LINQ(順次処理)はこうです。
using System;
using System.Linq;
var numbers = Enumerable.Range(1, 10_000_000);
var sw = System.Diagnostics.Stopwatch.StartNew();
var result = numbers
.Select(x => HeavyCalc(x)) // 重い計算だと仮定
.ToArray();
sw.Stop();
Console.WriteLine($"Sequential: {sw.ElapsedMilliseconds} ms");
int HeavyCalc(int x)
{
// ここでは単純に少し待つだけ
System.Threading.Thread.SpinWait(500);
return x * x;
}
C#これを「並列 LINQ」にするときにやることは、たった 1 つです。
var result = numbers
.AsParallel() // ここがポイント
.Select(x => HeavyCalc(x))
.ToArray();
C#AsParallel() を挟むだけで、「この後の LINQ を並列でやっていいよ」という宣言になります。
ここでの重要ポイントは、「並列 LINQ は“別のメソッド”ではなく、AsParallel() でモードを切り替えるだけ」という感覚を持つことです。
LINQ の書き方自体はほとんど変わりません。
並列 LINQ と順次 LINQ の違いをイメージでつかむ
順次 LINQ:1 人で全部やる
順次 LINQ は、イメージとしては「1 人の作業者が、先頭から順番に全部処理していく」感じです。
1 番目の要素を処理
2 番目の要素を処理
3 番目の要素を処理
…
CPU が 8 コアあっても、基本的には 1 コアしか使いません。
並列 LINQ:複数人で手分けする
並列 LINQ は、「複数人で手分けして処理する」イメージです。
スレッド A が 1~1000 番を処理
スレッド B が 1001~2000 番を処理
スレッド C が 2001~3000 番を処理
…
CPU のコアを複数使って、同時に処理を進めます。
ここでの重要ポイントは、「並列 LINQ は“同じ処理を大量のデータに対して行うときにだけ”意味がある」ということです。
1 件あたりの処理が軽い場合や、件数が少ない場合は、並列化のオーバーヘッドのほうが大きくなって逆に遅くなります。
並列 LINQ の基本的な書き方とオプション
AsParallel と AsSequential
一番基本の形はこれです。
var result = source
.AsParallel()
.Select(x => ...)
.Where(x => ...)
.ToList();
C#途中で「ここから先は順次に戻したい」ときは、AsSequential() を使います。
var result = source
.AsParallel()
.Select(x => HeavyCalc(x))
.AsSequential()
.OrderBy(x => x) // ここは順次で実行
.ToList();
C#ここでの重要ポイントは、「AsParallel 以降が並列モード、AsSequential 以降が通常モード」という切り替えになっていることです。
順序を保ちたいとき:AsOrdered
並列 LINQ は、基本的に「順序を保証しない」モードで動きます。
つまり、元の順番と違う順番で結果が返ってくることがあります。
順序を保ちたいときは、AsOrdered() を使います。
var result = numbers
.AsParallel()
.AsOrdered() // 元の順序を保つ
.Select(x => HeavyCalc(x))
.ToArray();
C#ただし、順序を保つために内部で余分な処理が入るので、速度は少し落ちます。
ここでの重要ポイントは、「順序が必要かどうか」を最初に決めることです。
順序が不要なら AsOrdered は付けないほうが速くなります。
並列 LINQ を使うときの「絶対に外せない注意点」
副作用のある処理を中に書かない
並列 LINQ の中で、こういうコードは危険です。
int sum = 0;
var result = numbers
.AsParallel()
.Select(x =>
{
sum += x; // 共有変数を更新している
return x * x;
})
.ToArray();
C#複数のスレッドが同時に sum を書き換えるので、結果が壊れます(レースコンディション)。
「たまに合わない」「環境によって結果が変わる」といった、厄介なバグの原因になります。
並列 LINQ の中では、
共有変数を書き換えない
外側のコレクションを直接変更しない
ログやコンソール出力も、できれば避ける(順序がぐちゃぐちゃになる)
というのが基本です。
ここでの重要ポイントは、「並列 LINQ の中の処理は“純粋な変換”に近づける」ということです。
入力を受け取って、結果を返すだけ。外の世界をいじらない。
このイメージを持っておくと、安全に使いやすくなります。
軽すぎる処理を並列化しても意味がない
例えば、こういうコードは並列化してもほぼ意味がありません。
var result = numbers
.AsParallel()
.Select(x => x * 2) // すごく軽い処理
.ToArray();
C#1 件あたりの処理が軽すぎると、「スレッドを分けるコスト」「結果をまとめるコスト」のほうが大きくなります。
結果として、順次 LINQ より遅くなることも普通にあります。
並列 LINQ が効きやすいのは、
1 件あたりの処理がそこそこ重い(CPU を使う計算など)
件数が多い(数万件~)
という条件がそろっているときです。
ここでの重要ポイントは、「なんでもかんでも AsParallel すれば速くなるわけではない」ということです。
“重い × 多い”ときだけ、候補に入れるくらいの感覚でいてください。
業務での「ここなら並列 LINQ を検討していい」具体例
例1:画像やファイルの重い変換処理
大量の画像ファイルを、別のサイズやフォーマットに変換するような処理は、CPU をかなり使います。
using System.Collections.Generic;
using System.IO;
using System.Linq;
IEnumerable<string> GetImageFiles(string dir)
{
return Directory.EnumerateFiles(dir, "*.jpg");
}
void ConvertAll(string dir)
{
var files = GetImageFiles(dir);
files
.AsParallel()
.ForAll(path =>
{
ConvertOne(path);
});
}
void ConvertOne(string path)
{
// 画像を読み込んで、リサイズして、保存する…など
}
C#ここでは Select ではなく ForAll を使っています。ForAll は「並列に foreach する」イメージのメソッドです。
ここでの重要ポイントは、「1 ファイルごとの処理が重く、他と独立している」ので、並列化と相性が良いということです。
例2:CPU を使う集計・シミュレーション
例えば、「大量のパラメータに対してシミュレーションを回す」ような処理も、並列化の候補になります。
var parameters = Enumerable.Range(1, 100_000);
var results = parameters
.AsParallel()
.Select(p => RunSimulation(p))
.ToArray();
double RunSimulation(int p)
{
// CPU を使う重い計算
double x = 0;
for (int i = 0; i < 1_000; i++)
{
x += Math.Sqrt(p * i + 1);
}
return x;
}
C#ここでの重要ポイントは、「各シミュレーションが互いに独立していて、共有状態を持たない」ことです。
こういう“独立した重い処理の集合”は、並列 LINQ の得意分野です。
まとめ:「並列 LINQ ユーティリティ」は“重い処理を安全に手分けするための道具”
並列 LINQ(PLINQ)の本質は、
同じ処理を大量のデータに対して行うときに、
複数のスレッドで手分けして、トータル時間を短くする
ことです。
押さえておきたいポイントをもう一度まとめると、
AsParallel() で「並列モード」に切り替える
順序が必要なら AsOrdered()、途中で順次に戻したければ AsSequential()
中の処理で共有変数を書き換えない(副作用を避ける)
1 件あたりの処理が重くて、件数が多いときだけ効果が出やすい
画像変換・シミュレーション・重い計算など、“独立した重い処理の集合”と相性が良い
ここまで腹落ちしていれば、
「なんとなく AsParallel を付けてみる」段階から卒業して、
“どこで並列 LINQ を使うべきか/使うべきでないか”を、自分で判断できるようになります。
