Parallel LINQ (PLINQ) は、 Language-Integrated クエリ (LINQ) パターンの並列実装です。 PLINQ は、 System.Linq 名前空間の拡張メソッドとして LINQ 標準クエリ演算子の完全なセットを実装し、並列操作用の追加の演算子を備えています。 PLINQ は、LINQ 構文のシンプルさと読みやすさと並列プログラミングの機能を組み合わせたものになっています。
ヒント
LINQ に慣れていない場合は、型セーフな方法で列挙可能なデータ ソースに対してクエリを実行するための統合モデルが用意されています。 LINQ to Objects は、 List<T> や配列などのメモリ内コレクションに対して実行される LINQ クエリの名前です。 この記事では、LINQ について基本的な理解があることを前提としています。 詳細については、「 Language-Integrated クエリ (LINQ)」を参照してください。
並列クエリとは
PLINQ クエリは、並列でない LINQ to Objects クエリに似ています。 PLINQ クエリは、シーケンシャル LINQ クエリと同様に、メモリ内の IEnumerable または IEnumerable<T> データ ソースで動作し、遅延実行を行います。つまり、クエリが列挙されるまで実行を開始しません。 主な違いは、PLINQ がシステム上のすべてのプロセッサを最大限に活用しようとする点です。 これを行うには、データ ソースをセグメントにパーティション分割し、複数のプロセッサで個別のワーカー スレッドで各セグメントに対してクエリを並列実行します。 多くの場合、並列実行はクエリの実行速度が大幅に向上することを意味します。
並列実行により、PLINQ は、多くの場合、 AsParallel クエリ操作をデータ ソースに追加するだけで、特定の種類のクエリのレガシ コードよりも大幅なパフォーマンス向上を実現できます。 ただし、並列処理では独自の複雑さが生じる可能性があり、すべてのクエリ操作が PLINQ で高速に実行されるわけではありません。 実際、並列化では、実際には特定のクエリの速度が低下します。 したがって、順序付けなどの問題が並列クエリにどのように影響するかを理解する必要があります。 詳細については、「 PLINQ の高速化について」を参照してください。
注
このドキュメントでは、ラムダ式を使用して PLINQ でデリゲートを定義します。 C# または Visual Basic のラムダ式に慣れていない場合は、PLINQ と TPL のラムダ式のを参照してください。
この記事の残りの部分では、PLINQ の主要なクラスの概要と、PLINQ クエリを作成する方法について説明します。 各セクションには、より詳細な情報とコード例へのリンクが含まれています。
ParallelEnumerable クラス
System.Linq.ParallelEnumerable クラスは、PLINQ のほぼすべての機能を公開します。 それとその他の System.Linq 名前空間の型は、System.Core.dll アセンブリにコンパイルされます。 Visual Studio の既定の C# プロジェクトと Visual Basic プロジェクトは、アセンブリを参照し、名前空間をインポートします。
ParallelEnumerable には、LINQ to Objects がサポートするすべての標準クエリ演算子の実装が含まれていますが、それぞれの並列化は試みません。 LINQ に慣れていない場合は、 LINQ の概要 (C#) と LINQ の 概要 (Visual Basic) に関するページを参照してください。
標準のクエリ演算子に加えて、 ParallelEnumerable クラスには、並列実行に固有の動作を可能にする一連のメソッドが含まれています。 これらの PLINQ 固有のメソッドを次の表に示します。
ParallelEnumerable 演算子 | 説明 |
---|---|
AsParallel | PLINQ のエントリ ポイント。 可能であれば、クエリの残りの部分を並列化することを指定します。 |
AsSequential | クエリの残りの部分を非並列 LINQ クエリとして順番に実行するように指定します。 |
AsOrdered | PLINQ で、クエリの残りの部分のソース シーケンスの順序を保持するか、順序が変更されるまで (たとえば、Orderby (Visual Basic の Order By) 句を使用して) 保持する必要があることを指定します。 |
AsUnordered | ソース シーケンスの順序を保持するために、クエリの残りの部分の PLINQ が必要ないことを指定します。 |
WithCancellation | 指定されたキャンセル トークンの状態を PLINQ が定期的に監視し、要求された場合は実行を取り消す必要があることを指定します。 |
WithDegreeOfParallelism | クエリの並列化に PLINQ が使用するプロセッサの最大数を指定します。 |
WithMergeOptions | PLINQ が可能であれば、並列結果を使用スレッド上の 1 つのシーケンスにマージする方法に関するヒントを提供します。 |
WithExecutionMode | 既定の動作が順番に実行される場合でも、PLINQ でクエリを並列化するかどうかを指定します。 |
ForAll | クエリの結果を反復処理するのとは異なり、最初にコンシューマー スレッドにマージすることなく、結果を並列で処理できるようにするマルチスレッド列挙メソッド。 |
Aggregate オーバーロード | PLINQ に固有のオーバーロード。スレッド ローカル パーティションに対する中間集計と、すべてのパーティションの結果を結合する最終的な集計関数。 |
オプトイン モデル
クエリを記述するときは、次の例に示すように、データ ソースで ParallelEnumerable.AsParallel 拡張メソッドを呼び出して PLINQ にオプトインします。
var source = Enumerable.Range(1, 10000);
// Opt in to PLINQ with AsParallel.
var evenNums = from num in source.AsParallel()
where num % 2 == 0
select num;
Console.WriteLine($"{evenNums.Count()} even numbers out of {source.Count()} total");
// The example displays the following output:
// 5000 even numbers out of 10000 total
Dim source = Enumerable.Range(1, 10000)
' Opt in to PLINQ with AsParallel
Dim evenNums = From num In source.AsParallel()
Where num Mod 2 = 0
Select num
Console.WriteLine("{0} even numbers out of {1} total",
evenNums.Count(), source.Count())
' The example displays the following output:
' 5000 even numbers out of 10000 total
AsParallel拡張メソッドは、後続のクエリ演算子 (この場合はwhere
とselect
) をSystem.Linq.ParallelEnumerable実装にバインドします。
実行モード
既定では、PLINQ は保守的です。 実行時に、PLINQ インフラストラクチャはクエリの全体的な構造を分析します。 並列化によってクエリが高速化される可能性がある場合、PLINQ はソース シーケンスを同時に実行できるタスクに分割します。 クエリを並列化しても安全でない場合、PLINQ はクエリを順番に実行するだけです。 PLINQ で、コストの高い並列アルゴリズムと安価なシーケンシャル アルゴリズムのどちらかを選択する場合は、既定でシーケンシャル アルゴリズムが選択されます。 WithExecutionModeメソッドとSystem.Linq.ParallelExecutionMode列挙体を使用して、並列アルゴリズムを選択するように PLINQ に指示できます。 これは、特定のクエリの実行速度が並列で速いことがわかっている場合に、テストと測定によって役立ちます。 詳細については、「 方法: PLINQ で実行モードを指定する」を参照してください。
並列処理の次数
既定では、PLINQ はホスト コンピューター上のすべてのプロセッサを使用します。 WithDegreeOfParallelismメソッドを使用して、指定した数以下のプロセッサを使用するように PLINQ に指示できます。 これは、コンピューター上で実行されている他のプロセスが一定の CPU 時間を受け取るようにする場合に便利です。 次のスニペットでは、クエリが最大 2 つのプロセッサを使用するように制限しています。
var query = from item in source.AsParallel().WithDegreeOfParallelism(2)
where Compute(item) > 42
select item;
Dim query = From item In source.AsParallel().WithDegreeOfParallelism(2)
Where Compute(item) > 42
Select item
ファイル I/O など、クエリで大量の非コンピューティングバインド処理が実行されている場合は、コンピューター上のコア数より大きい並列処理の程度を指定すると便利な場合があります。
順序付けされた並列クエリと順序なし並列クエリ
一部のクエリでは、クエリ演算子はソース シーケンスの順序を保持する結果を生成する必要があります。 PLINQ は、この目的のために AsOrdered 演算子を提供します。 AsOrdered は AsSequentialとは異なります。 AsOrdered シーケンスは引き続き並列で処理されますが、その結果はバッファーに格納され、並べ替えられます。 通常、順序の保持には余分な作業が伴うため、既定のAsUnordered シーケンスよりもAsOrdered シーケンスの処理が遅くなる可能性があります。 特定の順序付けられた並列操作が順次バージョンの操作よりも高速かどうかは、多くの要因によって異なります。
次のコード例は、順序の保持をオプトインする方法を示しています。
var evenNums =
from num in numbers.AsParallel().AsOrdered()
where num % 2 == 0
select num;
Dim evenNums = From num In numbers.AsParallel().AsOrdered()
Where num Mod 2 = 0
Select num
詳細については、「 PLINQ での注文の保持」を参照してください。
並列クエリとシーケンシャル クエリ
一部の操作では、ソース データを順番に配信する必要があります。 ParallelEnumerableクエリ演算子は、必要に応じて自動的にシーケンシャル モードに戻ります。 シーケンシャル実行を必要とするユーザー定義クエリ演算子とユーザー デリゲートの場合、PLINQ には AsSequential メソッドが用意されています。 AsSequentialを使用すると、AsParallelが再度呼び出されるまで、クエリ内の後続のすべての演算子が順番に実行されます。 詳細については、「 方法: 並列 LINQ クエリとシーケンシャル LINQ クエリを結合する」を参照してください。
クエリ結果をマージするためのオプション
PLINQ クエリが並列で実行される場合、各ワーカー スレッドからの結果をメイン スレッドにマージして、 foreach
ループ (Visual Basic でFor Each
) で使用するか、リストまたは配列に挿入する必要があります。 場合によっては、特定の種類のマージ操作を指定すると便利な場合があります。たとえば、結果の生成をより迅速に開始できます。 このため、PLINQ では、 WithMergeOptions メソッドと ParallelMergeOptions 列挙型がサポートされています。 詳細については、「 PLINQ のマージ オプション」を参照してください。
ForAll 演算子
順次 LINQ クエリでは、クエリが foreach
(Visual Basic のFor Each
) ループで列挙されるか、 ToList 、 ToArray 、 ToDictionaryなどのメソッドを呼び出すまで遅延されます。 PLINQ では、 foreach
を使用してクエリを実行し、結果を反復処理することもできます。 ただし、 foreach
自体は並列で実行されないため、すべての並列タスクからの出力を、ループが実行されているスレッドにマージし直す必要があります。 PLINQ では、クエリ結果の最終的な順序を保持する必要がある場合や、各要素のConsole.WriteLine
を呼び出す場合など、シリアル方式で結果を処理する場合に、foreach
を使用できます。 順序の保持が不要で、結果自体の処理を並列化できる場合は、 ForAll メソッドを使用して PLINQ クエリを実行すると、クエリの実行速度が向上します。
ForAll では、この最後のマージ 手順は実行されません。 次のコード例は、 ForAll メソッドの使用方法を示しています。
System.Collections.Concurrent.ConcurrentBag<T> は、項目を削除せずに同時に追加する複数のスレッド用に最適化されているため、ここで使用されます。
var nums = Enumerable.Range(10, 10000);
var query =
from num in nums.AsParallel()
where num % 10 == 0
select num;
// Process the results as each thread completes
// and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
// which can safely accept concurrent add operations
query.ForAll(e => concurrentBag.Add(Compute(e)));
Dim nums = Enumerable.Range(10, 10000)
Dim query = From num In nums.AsParallel()
Where num Mod 10 = 0
Select num
' Process the results as each thread completes
' and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
' which can safely accept concurrent add operations
query.ForAll(Sub(e) concurrentBag.Add(Compute(e)))
次の図は、クエリの実行に関する foreach
と ForAll の違いを示しています。
キャンセル
PLINQ は、.NET のキャンセルの種類と統合されています。 (詳細については、「 マネージド スレッドでの取り消し」を参照してください)。そのため、連続する LINQ to Objects クエリとは異なり、PLINQ クエリを取り消すことができます。 キャンセル可能な PLINQ クエリを作成するには、クエリで WithCancellation 演算子を使用し、引数として CancellationToken インスタンスを指定します。 トークンの IsCancellationRequested プロパティが true に設定されている場合、PLINQ はそれに気付き、すべてのスレッドでの処理を停止し、 OperationCanceledExceptionをスローします。
取り消しトークンが設定された後も、PLINQ クエリが一部の要素を処理し続ける可能性があります。
応答性を高めるために、実行時間の長いユーザー デリゲートのキャンセル要求に応答することもできます。 詳細については、「 方法: PLINQ クエリをキャンセルする」を参照してください。
例外
PLINQ クエリを実行すると、異なるスレッドから複数の例外が同時にスローされる可能性があります。 また、例外を処理するコードは、例外をスローしたコードとは異なるスレッド上にある可能性があります。 PLINQ では、 AggregateException 型を使用して、クエリによってスローされたすべての例外をカプセル化し、それらの例外を呼び出し元のスレッドにマーシャリングします。 呼び出し元のスレッドでは、try-catch ブロックが 1 つだけ必要です。 ただし、 AggregateException にカプセル化されているすべての例外を反復処理し、安全に復旧できる例外をキャッチできます。 まれに、 AggregateExceptionでラップされていない一部の例外がスローされる場合があり、 ThreadAbortExceptionもラップされません。
例外が結合スレッドにバブル アップすることが許可されている場合、例外が発生した後もクエリが一部の項目を処理し続ける可能性があります。
詳細については、「 方法: PLINQ クエリで例外を処理する」を参照してください。
カスタム パーティショナー
場合によっては、ソース データの特性を利用するカスタム パーティショナーを記述することで、クエリのパフォーマンスを向上させることができます。 クエリでは、カスタム パーティショナー自体がクエリされる列挙可能なオブジェクトです。
int[] arr = new int[9999];
Partitioner<int> partitioner = new MyArrayPartitioner<int>(arr);
var query = partitioner.AsParallel().Select(SomeFunction);
Dim arr(10000) As Integer
Dim partitioner As Partitioner(Of Integer) = New MyArrayPartitioner(Of Integer)(arr)
Dim query = partitioner.AsParallel().Select(Function(x) SomeFunction(x))
PLINQ では、固定数のパーティションがサポートされています (ただし、負荷分散の実行時に、データがそれらのパーティションに動的に再割り当てされる可能性があります)。 For と ForEach は動的パーティション分割のみをサポートします。つまり、パーティションの数は実行時に変更されます。 詳細については、「 PLINQ と TPL のカスタム パーティショナー」を参照してください。
PLINQ のパフォーマンスの測定
多くの場合、クエリを並列化できますが、並列クエリを設定するオーバーヘッドが、得られるパフォーマンス上の利点を上回ります。 クエリがあまり計算を実行しない場合、またはデータ ソースが小さい場合、PLINQ クエリは、連続する LINQ to Objects クエリよりも遅くなる可能性があります。 Visual Studio Team Server の Parallel Performance Analyzer を使用すると、さまざまなクエリのパフォーマンスを比較したり、処理のボトルネックを特定したり、クエリが並列で実行されているか順番に実行されているかを判断したりできます。 詳細については、「 コンカレンシー ビジュアライザー 」と「 方法: PLINQ クエリのパフォーマンスを測定する」を参照してください。
こちらも参照ください
.NET