PLINQ の非利便性

PLINQ を使用すると、多くの場合、連続した LINQ to Objects クエリのパフォーマンスが大幅に向上します。 ただし、クエリの実行を並列化すると、複雑性が増すため、シーケンシャルなコードにおいて一般的でない問題、またはめったにない問題を引き起こす場合があります。 このトピックでは、PLINQ クエリを記述するときに回避すべき点について示します。

並列処理が常に高速であると思い込まない

並列化することで、PLINQ クエリの実行速度が同等の LINQ to Objects より遅くなる場合があります。 基本的な経験則では、ソース要素が少なく、高速ユーザー デリゲートを使用するクエリの速度が大幅に向上することはほとんどありません。 ただし、パフォーマンスには多くの要因が関係するため、実際の結果を測定してから、PLINQ を使用するかどうかを決定することをお勧めします。 詳細については、「Understanding Speedup in PLINQ (PLINQ での高速化について)」を参照してください。

共有メモリの位置への書き込みを回避する

逐次コードでは、静的変数またはクラス フィールドから読み取ることや、これらの場所に書き込むことはよくあります。 ただし、複数のスレッドからこのような変数に同時にアクセスしているときは、著しい競合状態になる場合がよくあります。 ロックを使用して変数へのアクセスを同期できる場合でも、同期のコストでパフォーマンスが低下する可能性があります。 このため、PLINQ クエリにおける共有状態へのアクセスは、できるだけ回避するか、少なくとも制限することをお勧めします。

過剰な並列化を回避する

AsParallel のメソッドを使用することで、ソース コレクションのパーティション分割とワーカー スレッドの同期によるオーバーヘッド コストが発生します。 並列化の利点は、コンピューター上のプロセッサ数によってさらに制限されます。 1 つのプロセッサで複数の計算主体のスレッドを実行しても、高速化は実現しません。 そのため、クエリを過剰に並列処理しないように注意する必要があります。

過剰な並列化が発生する可能性が特に高い一般的な状況は、次のスニペットで示すように、入れ子になったクエリの場合です。

var q = from cust in customers.AsParallel()
        from order in cust.Orders.AsParallel()
        where order.OrderDate > date
        select new { cust, order };
Dim q = From cust In customers.AsParallel()
        From order In cust.Orders.AsParallel()
        Where order.OrderDate > aDate
        Select New With {cust, order}

この場合、次の条件のうち 1 つ以上該当しない限り、外側のデータ ソース (customers) のみを並列化することをお勧めします。

  • 内部データ ソース (cust.Orders) が非常に長い。

  • 各順序で高負荷の計算を実行している (この例に示す操作の負荷は大きくありません)。

  • 対象システムに、cust.Orders でクエリを並列化することで生成されるスレッドの数を十分に処理できるプロセッサが存在している。

どの場合も、最適なクエリの形式を決定する最善の方法は、テストおよび測定することです。 詳細については、PLINQ クエリのパフォーマンスを測定する」をご覧ください。

スレッド セーフでないメソッドの呼び出しを回避する

PLINQ クエリからスレッド セーフでないインスタンス メソッドに書き込むと、プログラムで検出されない可能性のあるデータ破損が起こる可能性があります。 例外が発生する可能性もあります。 次の例では、複数のスレッドが、クラスでサポートされていない FileStream.Write メソッドの同時呼び出しを試みます。

Dim fs As FileStream = File.OpenWrite(…)
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(Sub(x) fs.Write(x))
FileStream fs = File.OpenWrite(...);
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(x => fs.Write(x));

スレッド セーフなメソッドの呼び出しを制限する

.NET のほとんどの静的メソッドはスレッド セーフであり、複数のスレッドから同時に呼び出すことができます。 ただし、このような場合でも、関連する同期によっては、クエリの処理速度が大幅に低下する可能性があります。

注意

これは、クエリに WriteLine の呼び出しをいくつか挿入することで自分でテストできます。 このメソッドは、ドキュメントの例でデモのために使用されていますが、PLINQ クエリでは使用しないでください。

不要な並べ替え操作を回避する

PLINQ を使用して 1 つのクエリを並列で実行すると、ソース シーケンスがパーティションに分割され、複数のスレッド上で同時に操作できるようになります。 既定では、パーティションが処理される順序と生成される結果は予測できません (OrderBy などの演算子を除く)。 PLINQ に、ソース シーケンスの順序を維持するように指示することもできますが、この操作はパフォーマンスの低下を招きます。 推奨される手順は、できる限り、順序の維持に依存しないようにクエリを構成することです。 詳細については、「Order Preservation in PLINQ (PLINQ における順序維持)」を参照してください。

できる限り ForEach より ForAll を優先する

PLINQ では複数のスレッドで 1 つのクエリを実行しますが、foreach ループ (Visual Basic の場合は For Each) の結果を処理する場合、クエリ結果は 1 つのスレッドにマージされてから列挙子によって連続してアクセスされる必要があります。 これが避けられない場合もありますが、できる限り ForAll メソッドを使用して各スレッドで固有の結果を出力できるようにします。たとえば、System.Collections.Concurrent.ConcurrentBag<T> などのスレッド セーフなコレクションに書き込みます。

同じイシューが Parallel.ForEach に適用されます。 つまり、source.AsParallel().Where().ForAll(...)Parallel.ForEach(source.AsParallel().Where(), ...) よりも強く求められます。

スレッド アフィニティのイシューに注意する

シングル スレッド アパートメント (STA) コンポーネント向けの COM 相互運用性、Windows フォーム、Windows Presentation Foundation (WPF) などの一部のテクノロジでは、特定のスレッド上で実行するコードを必要とするスレッド アフィニティが制限される場合があります。 たとえば、Windows フォームと WPF では、コントロールへのアクセスは、そのコントロールが作成されたスレッド上でしか行うことができません。 PLINQ クエリで Windows フォーム コントロールの共有状態にアクセスしようとすると、デバッガーを実行中である場合は例外が発生します (この設定はオフにできます)。ただし、クエリが UI スレッドで処理される場合、コードは 1 つのスレッドでのみ実行されるので、クエリ結果を列挙する foreach ループからコントロールにアクセスできます。

ForEach、For および ForAll のイテレーションが必ず並列実行されているとは限らない

Parallel.ForParallel.ForEach、または ForAll の各ループにおける個々のイテレーションが必ずしも並列実行されるとは限らないことに注意してください。 そのため、イテレーションの並列実行の正確性、または特定の順序でのイテレーションの実行の正確性に依存するコードを記述しないでください。

たとえば、次のコードはデッドロックが起こる可能性があります。

Dim mre = New ManualResetEventSlim()
Enumerable.Range(0, Environment.ProcessorCount * 100).AsParallel().ForAll(Sub(j)
   If j = Environment.ProcessorCount Then
       Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
       mre.Set()
   Else
       Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
       mre.Wait()
   End If
End Sub) ' deadlocks
ManualResetEventSlim mre = new ManualResetEventSlim();
Enumerable.Range(0, Environment.ProcessorCount * 100).AsParallel().ForAll((j) =>
{
    if (j == Environment.ProcessorCount)
    {
        Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
        mre.Set();
    }
    else
    {
        Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
        mre.Wait();
    }
}); //deadlocks

この例では、1 つのイテレーションでイベントを設定し、その他のすべてのイテレーションでイベントを待機します。 待機のイテレーションは、イベント設定のイテレーションが完了するまで完了できません。 ただし、待機のイテレーションによって、並列ループの実行に使用されるすべてのスレッドがブロックされ、イベント設定のイテレーションがまったく実行されなくなる可能性があります。 これにより、イベント設定のイテレーションが実行されず、待機のイテレーションが開始されないままの状態になるデッドロックが発生します。

具体的には、処理を適切に進めるには、並列ループの特定のイテレーションでそのループの別のイテレーションを待機するのは避ける必要があります。 並列ループで、イテレーションが逆の順序で順次スケジュールされた場合、デッドロックが発生します。

関連項目