C#のLINQ(Language Integrated Query)は非常に便利ですが、標準の IEnumerable<T> に対するクエリはシングルスレッドでシーケンシャル(順次)に実行されます。
大量のデータ集計やCPU負荷の高い計算を行う場合、PLINQ (Parallel LINQ) を導入することで、マルチコアCPUのリソースを有効活用し、処理速度を劇的に向上させることが可能です。
ここでは、AsParallel 拡張メソッドを使用して、既存のLINQクエリを簡単に並列化する方法を解説します。
AsParallelによる並列クエリの実装
通常のLINQクエリに .AsParallel() を追加するだけで、クエリ全体が並列実行モード(ParallelQuery<T>)に切り替わります。また、WithDegreeOfParallelism を使用することで、同時実行するスレッド数の上限を制御できます。
以下のコードは、複数の広告キャンペーンデータからクリック率(CTR)を計算する処理を並列で行う例です。
サンプルコード
using System;
using System.Linq;
using System.Threading;
public class Program
{
public static void Main()
{
// 1. データソースの定義(広告キャンペーンの配信実績)
var campaigns = new[]
{
new { CampaignName = "Summer Sale A", Impressions = 150000, Clicks = 4500 },
new { CampaignName = "Winter Promo B", Impressions = 120000, Clicks = 3200 },
new { CampaignName = "New Arrival C", Impressions = 200000, Clicks = 8500 },
new { CampaignName = "Clearance D", Impressions = 80000, Clicks = 1200 },
new { CampaignName = "Member Only E", Impressions = 50000, Clicks = 2500 },
};
Console.WriteLine("--- 解析処理開始 ---");
// 2. PLINQによる並列処理クエリの構築
var query = campaigns
// 順次クエリを並列クエリに変換
.AsParallel()
// 同時実行スレッド数を指定(環境や負荷に応じて調整)
.WithDegreeOfParallelism(4)
// 順序を保持したい場合は AsOrdered() を追加するが、
// パフォーマンス重視の場合は指定しない(順序は保証されなくなる)
.Select(c => new
{
Name = c.CampaignName,
// クリック率(CTR)の計算
Ctr = (double)c.Clicks / c.Impressions * 100.0,
// 実行スレッドIDを取得して並列動作を確認
ProcessedThreadId = Thread.CurrentThread.ManagedThreadId
});
// 3. 結果の列挙
// PLINQは遅延実行されるため、foreachで回した時点で計算が走ります
foreach (var item in query)
{
Console.WriteLine(
$"キャンペーン: {item.Name, -15} | CTR: {item.Ctr:F2}% (Thread: {item.ProcessedThreadId})"
);
}
}
}
解説と技術的なポイント
1. AsParallel拡張メソッド
System.Linq 名前空間に含まれるこのメソッドを呼び出すと、データソースが分割(パーティショニング)され、複数のスレッドで同時に処理されるようになります。特に Select や Where 句の中身が複雑な計算を含む場合、大幅なパフォーマンス向上が期待できます。
2. WithDegreeOfParallelism
デフォルトでは、PLINQはCPUの全コアを使用しようとします。WithDegreeOfParallelism を使用すると、使用するコア(スレッド)の最大数を制限できます。これは、サーバーアプリケーションなどで特定のリソースを専有しすぎないようにする場合に有効です。
3. 実行順序と副作用
PLINQを使用した並列処理では、結果の順序は保証されません。もし入力データの順序通りに出力する必要がある場合は、.AsOrdered() を追加してください(ただし、並べ替えのコストが発生するため速度は若干低下します)。
また、並列実行されるデリゲート(Select 内の処理など)から、外部の変数やリストに対して書き込みを行うと、スレッド競合(レースコンディション)が発生する危険があります。PLINQ内では、副作用のない純粋な計算処理を行うように設計するのがベストプラクティスです。
