非同期タスクと並列タスクを管理する

完了

C# 開発者にとって、タスク並列ライブラリ (TPL) を使用すると、並列コードを簡単に記述できます。 ただし、すべてのコードが並列化に適しているわけではありません。 たとえば、ループが各イテレーションに対して少量の作業のみを実行する場合、または多数のイテレーションで実行されない場合、並列化のオーバーヘッドにより、コードの実行速度が低下する可能性があります。 さらに、マルチスレッド コードと同様に、並列化によってプログラムの実行が複雑になります。

データとタスクの並列処理における一般的な落とし穴

Parallel.For および Parallel.ForEach を使用すると、多くの場合、通常の順次ループよりもパフォーマンスが大幅に向上します。 ただし、ループを並列化する作業では複雑さが生じ、シーケンシャル コードでは一般的ではない問題が発生する可能性があります。

並列が常に高速であると想定しないでください

並列ループは、場合によっては対応する順次処理よりも時間がかかる可能性があります。 基本的な経験則は、反復回数が少なく、高速なユーザー デリゲートを持つ並列ループが大幅に高速化される可能性が低いということです。 ただし、パフォーマンスには多くの要因が関係するため、常に実際の結果を測定することをお勧めします。

共有メモリの場所への書き込みを避ける

シーケンシャル コードでは、静的変数またはクラス フィールドから読み取ったり、静的な変数やクラス フィールドに書き込んだりすることは珍しくありません。 ただし、複数のスレッドがこのような変数に同時にアクセスする場合は常に、競合状態に大きな可能性があります。 ロックを使用して変数へのアクセスを同期できる場合でも、同期のコストでパフォーマンスが低下する可能性があります。 そのため、並列ループにおける共有状態へのアクセスは、可能な限り回避するか、少なくとも制限することをお勧めします。 この場合の最適な方法は、ループの実行中に Parallel.For 変数を使用してスレッド ローカルの状態を格納する、Parallel.ForEach および System.Threading.ThreadLocal<T> のオーバーロードを使用することです。

過剰並列化を回避する

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

過剰な並列化が発生する可能性が特に高い一般的な状況が、入れ子になったループ内です。 ほとんどの場合、次の条件の 1 つ以上が適用されない限り、外側のループのみを並列化することをお勧めします。

  • 内部ループは長いことがわかっている。
  • 各注文でコストの高い計算を実行しています。
  • ターゲット システムには、処理を並列化することによって生成されるスレッドの数を処理するのに十分なプロセッサがあることがわかっています。

どの場合も、最適なクエリの形式を決定する最善の方法は、テストおよび測定することです。

非同期タスクと並列タスクでの例外処理

タスク並列ライブラリ (TPL) を使用してタスクを実行すると、いくつかの異なる方法で例外が発生する可能性があります。 最も一般的なのは、タスクが例外を発生させる場合です。 タスクがスレッドプールのスレッドで実行されている場合や、メインスレッドで実行されている場合に、例外が投げられることがあります。 どちらの場合も、例外は呼び出し元のスレッドに反映されます。

Task.Wait メソッドを使用してタスクの完了を待機すると、タスクによってスローされたすべての例外が呼び出し元のスレッドに反映されます。 これらの例外は、try/catch ブロックを使用して処理できます。 タスクが、アタッチされた子タスクの親である場合、または複数のタスクを待機している場合、複数の例外がスローされることがあります。 1 つ以上の例外がスローされると、それらは AggregateException インスタンスにラップされます。

AggregateException例外には、スローされたすべての元の例外を調べ、それぞれを個別に処理 (または処理しない) するために列挙できるInnerExceptions プロパティがあります。

次の例では、タスクでスローされた例外を処理する方法を示しています。


public static partial class Program
{
    public static void Main()
    {
        HandleThree();
    }
    
    public static void HandleThree()
    {
        var task = Task.Run(
            () => throw new CustomException("This exception is expected!"));

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.InnerExceptions)
            {
                // Handle the custom exception.
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                // Rethrow any other exception.
                else
                {
                    throw ex;
                }
            }
        }
    }
}

// Define the CustomException class
public class CustomException : Exception
{
    public CustomException(string message) : base(message) { }
}
// The example displays the following output:
//        This exception is expected!

この例では、HandleThree メソッドにより、CustomException をスローするタスクが生成されます。 try/catch ブロックは、AggregateExceptionをキャッチし、InnerExceptions コレクションを反復処理します。 例外の種類が CustomException の場合、コンソールにメッセージが出力されます。 他の種類の例外の場合は、再スローされます。

AggregateException.Handle メソッドを使用して、元の例外を処理することもできます。 このメソッドは、 InnerExceptions コレクション内の各例外に対して呼び出されるデリゲートを受け取ります。 デリゲートから true が返された場合、例外は処理済みと見なされ、コレクションから削除されます。 false が返された場合、例外が再スローされます。

次の例では、 Handle メソッドを使用してタスクによってスローされた例外を処理する方法を示します。


public static partial class Program
{
    public static void HandleFour()
    {
        var task = Task.Run(
            () => throw new CustomException("This exception is expected!"));

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            ae.Handle(ex =>
            {
                // Handle the custom exception.
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                    return true;
                }
                // Rethrow any other exception.
                return false;
            });
        }
    }
}

この例では、HandleFour メソッドにより、CustomException をスローするタスクが生成されます。 try/catch ブロックは、AggregateExceptionをキャッチし、Handle メソッドを呼び出します。 デリゲートは、例外が CustomException型であるかどうかを確認します。 例外が CustomException型の場合、デリゲートはメッセージをコンソールに出力し、 trueを返します。 trueの応答は、例外が処理されたことを示します。 例外が他の種類の例外である場合、デリゲートは falseを返し、例外が再スローされます。

概要

このユニットでは、コードが並列処理に適していない状況について説明し、データとタスクの並列処理における一般的な落とし穴について説明します。 たとえば、並列が常に高速であると仮定すると、共有メモリの場所への書き込み、および過剰並列化が行われます。 また、 Task.Wait メソッドと AggregateException.Handle メソッドの使用方法など、非同期タスクと並列タスクで例外を処理する方法についても説明します。

重要なポイント

  • すべてのコードが並列化に適しているわけではありません。 コードを並列化する前に、パフォーマンスのテストと測定が不可欠です。
  • データとタスクの並列処理における一般的な落とし穴には、並列が常に高速であると想定する、共有メモリの場所への書き込み、および過剰並列化が含まれます。
  • 非同期タスクと並列タスクの例外は、 Task.Wait メソッドと AggregateException.Handle メソッドを使用して処理できます。
  • AggregateException例外には、InnerExceptionsプロパティがあり、これを列挙することで、スローされたすべての元の例外を調べることができます。