Parallel LINQ (PLINQ) を使用してデータ処理を並列化すると、デフォルトではパフォーマンスを最優先するため、処理結果の順序は入力データの順序とは一致しなくなります(順不同になります)。
しかし、時系列データやランキングデータのように「並び順」そのものに意味がある場合、並列化の恩恵を受けつつも、最終的な結果は元の順序通りに受け取りたいケースがあります。
今回は AsOrdered 拡張メソッドを使用して、並列処理を行いながら入力順序を維持する方法を解説します。
AsOrderedを使用した順序保存並列クエリ
PLINQクエリのチェーンに .AsOrdered() を追加することで、LINQランタイムは処理結果をバッファリングし、元のインデックスに基づいて正しく並べ直してから出力するようになります。
以下のサンプルコードでは、時系列に記録されたサーバーのアクセスログデータを解析するシナリオを想定しています。ログは発生順に並んでいる必要があり、並列処理後もその時系列順序を崩さないように実装します。
サンプルコード
using System;
using System.Linq;
using System.Threading;
public class Program
{
public static void Main()
{
// 1. データソース: 時系列のサーバーアクセスログ
// ID順(時系列順)に並んでいる前提
var accessLogs = new[]
{
new { LogId = 1001, Path = "/api/v1/users", LatencyMs = 120 },
new { LogId = 1002, Path = "/api/v1/auth", LatencyMs = 350 },
new { LogId = 1003, Path = "/home/index", LatencyMs = 45 },
new { LogId = 1004, Path = "/api/v1/data", LatencyMs = 800 },
new { LogId = 1005, Path = "/assets/img", LatencyMs = 20 },
new { LogId = 1006, Path = "/login", LatencyMs = 150 },
};
Console.WriteLine("--- 解析結果(順序維持) ---");
// 2. PLINQクエリの構築
var analyzedLogs = accessLogs
.AsParallel() // 並列化を有効にする
.AsOrdered() // ★入力データの順序を維持するよう指示
.WithDegreeOfParallelism(4) // 最大4スレッドで実行
.Select(log =>
{
// 擬似的な解析処理(スレッドIDを取得して並列実行を確認)
int threadId = Thread.CurrentThread.ManagedThreadId;
// 判定ロジック
string status = log.LatencyMs > 200 ? "SLOW" : "OK";
return new
{
log.LogId,
log.Path,
Result = status,
ProcessThread = threadId
};
});
// 3. 結果の出力
// Select内部はバラバラのスレッドで実行されるが、
// foreachで受け取る際は LogId: 1001, 1002... の順序が保証される
foreach (var item in analyzedLogs)
{
Console.WriteLine(
$"ID:{item.LogId} [{item.Result,-4}] {item.Path} (Thread:{item.ProcessThread})"
);
}
}
}
解説と技術的なポイント
1. AsOrderedの役割
通常、.AsParallel() の後に続く処理(Select や Where など)は、準備ができたものから順不同で出力されます。.AsOrdered() を挟むことで、PLINQは各要素にインデックスを割り当て、最終的な出力の段階で元の順番通りに整列されることを保証します。
2. パフォーマンスのトレードオフ
順序を維持するためには、並列処理された結果を一時的に保持し、同期をとって並べ替えるコスト(オーバーヘッド)が発生します。そのため、単純な .AsParallel()(順不同)に比べると処理速度は若干低下します。「順序が重要ではない」場合は、デフォルトの挙動(順不同)のままにするのがパフォーマンス上は有利です。
3. クエリの途中での使用
AsOrdered の効果は、後続の演算子(Take や Skip など)にも影響を与えます。例えば、AsParallel().Take(10) とすると「早く処理が終わった任意の10件」が取得されますが、AsParallel().AsOrdered().Take(10) とすれば「リストの先頭から10件」を確実に取得できます。
