C# Tips | コレクション・LINQ:並列LINQ

C# C#
スポンサーリンク

はじめに:「並列 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 を使うべきか/使うべきでないか”を、自分で判断できるようになります。

タイトルとURLをコピーしました