Share via


例外処理 (タスク並列ライブラリ)

タスク内で実行中のユーザー コードによってスローされた、ハンドルされない例外は、連結されているスレッドに反映されます。ただし、このトピックの後半で説明している任意の状況を除きます。 静的な、またはインスタンスの Task.Wait メソッドまたは Task<TResult>.Wait メソッドの 1 つを使用し、その呼び出しを try-catch ステートメントで囲んで例外を処理すると、例外が反映されます。 タスクが、アタッチされた子タスクの親である場合、または複数のタスクを待機している場合、複数の例外がスローされることがあります。 呼び出し元のスレッドにすべての例外を反映するために、Task インフラストラクチャが例外を AggregateException インスタンスにラップします。 AggregateException には、スローされた元のすべての例外を調べるために列挙できる InnerExceptions プロパティがあり、個々に処理したり未処理にしたりできます。 例外が 1 つだけスローされた場合でも、AggregateException でラップされます。

Dim task1 = Task.Factory.StartNew(Sub()
                                      Throw New MyCustomException("I'm bad, but not too bad!")
                                  End Sub)

Try
    task1.Wait()
Catch ae As AggregateException
    ' Assume we know what's going on with this particular exception.
    ' Rethrow anything else. AggregateException.Handle provides
    ' another way to express this. See later example.
    For Each ex In ae.InnerExceptions
        If TypeOf (ex) Is MyCustomException Then
            Console.WriteLine(ex.Message)
        Else
            Throw
        End If
    Next

End Try
var task1 = Task.Factory.StartNew(() =>
{
    throw new MyCustomException("I'm bad, but not too bad!");
});

try
{
    task1.Wait();
}
catch (AggregateException ae)
{
    // Assume we know what's going on with this particular exception.
    // Rethrow anything else. AggregateException.Handle provides
    // another way to express this. See later example.
    foreach (var e in ae.InnerExceptions)
    {
        if (e is MyCustomException)
        {
            Console.WriteLine(e.Message);
        }
        else
        {
            throw;
        }
    }

}

ハンドルされない例外は、AggregateException をキャッチして、内部例外を確認しないことで回避できます。 ただし、並列でない状況で基本例外の種類をキャッチする場合と類似しているため、この操作はお勧めしません。 特定の操作を取得することなく例外をキャッチして回復しようとすると、プログラムが中間状態のままになるおそれがあります。

例外を反映するタスクを待機しない場合、またはタスクの Exception プロパティにアクセスする場合、例外はガベージ コレクトされるときに .NET の例外ポリシーに従ってエスカレートされます。

連結しているスレッドへ例外が上方向に通知されると、例外が発生した後も、タスクによって一部の項目の処理が続行される可能性があります。

メモメモ

[マイ コードのみ] が有効になっている場合、Visual Studio では、例外をスローする行で処理が中断され、"ユーザー コードで処理されない例外" に関するエラー メッセージが表示されることがあります。 このエラーは問題にはなりません。F5 キーを押して続行し、以下の例に示す例外処理動作を確認できます。Visual Studio による処理が最初のエラーで中断しないようにするには、[ツール] メニューの [オプション]、[デバッグ] の順にクリックし、[全般] で [マイ コードのみ] チェック ボックスをオフにします。

アタッチされた子タスクと入れ子の AggregateExceptions

タスクに、例外をスローする子タスクがアタッチされている場合、その例外は親タスクに反映される前に AggregateException でラップされます。つまり、呼び出し元のスレッドに反映される前に、その例外が固有の AggregateException でラップされるということです。 このような場合、Task.WaitTask<TResult>.WaitWaitAny、または WaitAll の各メソッドでキャッチされた AggregateException の AggregateException().InnerExceptions プロパティには、違反の原因となった元の例外ではなく、1 つ以上の AggregateException インスタンスが含まれます。 入れ子の AggregateExceptions を反復処理しなくて済むようにするには、Flatten() メソッドを使用して入れ子の AggregateExceptions をすべて削除します。これにより、AggregateException() InnerExceptions プロパティに元の例外が含まれるようになります。 次の例では、入れ子の AggregateException インスタンスが 1 つのループで平坦化され、処理されています。

' task1 will throw an AE inside an AE inside an AE
Dim task1 = Task.Factory.StartNew(Sub()
                                      Dim child1 = Task.Factory.StartNew(Sub()
                                                                             Dim child2 = Task.Factory.StartNew(Sub()
                                                                                                                    Throw New MyCustomException("Attached child2 faulted.")
                                                                                                                End Sub,
                                                                                                                TaskCreationOptions.AttachedToParent)
                                                                         End Sub,
                                                                         TaskCreationOptions.AttachedToParent)
                                      ' Uncomment this line to see the exception rethrown.
                                      ' throw new MyCustomException("Attached child1 faulted.")
                                  End Sub)
Try
    task1.Wait()
Catch ae As AggregateException
    For Each ex In ae.Flatten().InnerExceptions
        If TypeOf (ex) Is MyCustomException Then
            Console.WriteLine(ex.Message)
        Else
            Throw
        End If
    Next
    'or like this:
    '  ae.Flatten().Handle(Function(e)
    '                               Return TypeOf (e) Is MyCustomException
    '                   End Function)
End Try
// task1 will throw an AE inside an AE inside an AE
var task1 = Task.Factory.StartNew(() =>
{
    var child1 = Task.Factory.StartNew(() =>
        {
            var child2 = Task.Factory.StartNew(() =>
            {
                throw new MyCustomException("Attached child2 faulted.");
            },
            TaskCreationOptions.AttachedToParent);

            // Uncomment this line to see the exception rethrown.
            // throw new MyCustomException("Attached child1 faulted.");
        },
        TaskCreationOptions.AttachedToParent);
});

try
{
    task1.Wait();
}
catch (AggregateException ae)
{

    foreach (var e in ae.Flatten().InnerExceptions)
    {
        if (e is MyCustomException)
        {
            // Recover from the exception. Here we just
            // print the message for demonstration purposes.
            Console.WriteLine(e.Message);
        }
        else
        {
            throw;
        }
    }
    // or ...
   // ae.Flatten().Handle((ex) => ex is MyCustomException);

}

デタッチされた子タスクの例外

既定では、子タスクはデタッチされた状態で作成されます。 デタッチされたタスクからスローされた例外は、処理されるか、直接の親に再スローされる必要があります。これらの例外は、アタッチされた子タスクが反映されるのとは異なる方法で、呼び出し元のスレッドに反映されます。 最上位の親では、デタッチされた子からの例外を手動で再スローすることで、AggregateException でラップして、連結されているスレッドに反映させることができます。

Dim task1 = Task.Factory.StartNew(Sub()
                                      Dim nestedTask1 = Task.Factory.StartNew(Sub()
                                                                                  Throw New MyCustomException("Nested task faulted.")
                                                                              End Sub)
                                      ' Here the exception will be escalated back to joining thread.
                                      ' We could use try/catch here to prevent that.
                                      nestedTask1.Wait()
                                  End Sub)
Try
    task1.Wait()
Catch ae As AggregateException
    For Each ex In ae.Flatten().InnerExceptions
        If TypeOf (ex) Is MyCustomException Then
            ' Recover from the exception. Here we just
            ' print the message for demonstration purposes.
            Console.WriteLine(ex.Message)
        End If
    Next
End Try
var task1 = Task.Factory.StartNew(() =>
{

    var nested1 = Task.Factory.StartNew(() =>
    {
        throw new MyCustomException("Nested task faulted.");
    });

    // Here the exception will be escalated back to joining thread.
    // We could use try/catch here to prevent that.
    nested1.Wait();

});

try
{
    task1.Wait();
}
catch (AggregateException ae)
{

    foreach (var e in ae.Flatten().InnerExceptions)
    {
        if (e is MyCustomException)
        {
            // Recover from the exception. Here we just
            // print the message for demonstration purposes.
            Console.WriteLine(e.Message);
        }
    }
}

子タスク内の例外を確認するために継続を使用する場合でも、親タスクによって例外を確認する必要があります。

他の処理との連携によるキャンセル処理を示す例外

タスク内のユーザー コードがキャンセル要求に応答する場合の正しいプロシージャは、その要求が伝えられたキャセル トークンに渡すために OperationCanceledException をスローすることです。 例外が反映される前に、タスク インスタンスによって、例外内のトークンがそのタスクが作成されたときに渡されたトークンと比較されます。 それらが同じである場合、タスクは AggregateException でラップされた TaskCanceledException を反映します。これは、内部例外を調べると確認できます。 ただし、連結しているスレッドがタスクで待機していない場合、このような特定の例外は反映されません。 詳細については、「タスクのキャンセル」を参照してください。

Dim someCondition As Boolean = True
Dim tokenSource = New CancellationTokenSource()
Dim token = tokenSource.Token

Dim task1 = Task.Factory.StartNew(Sub()
                                      Dim ct As CancellationToken = token
                                      While someCondition = True
                                          ' Do some work...
                                          Thread.SpinWait(500000)
                                          ct.ThrowIfCancellationRequested()
                                      End While
                                  End Sub,
                                  token)
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;

var task1 = Task.Factory.StartNew(() =>
{
    CancellationToken ct = token;
    while (someCondition)
    {
        // Do some work...
        Thread.SpinWait(50000);
        ct.ThrowIfCancellationRequested();
    }
},
token);

// No waiting required.

内部例外をフィルター処理する Handle メソッドの使用

Handle() メソッドを使用すると、"処理済み" として扱うことのできる例外をフィルターで除外できます。追加のロジックを使用する必要はありません。 Handle() に用意されているユーザー デリゲートでは、例外の種類、その Message() プロパティ、またはその例外に問題がないかどうかを判別できるその他の情報を調べることができます。 デリゲートが false を返す例外は、Handle() が返された直後に新しい AggregateException インスタンスで再スローされます。

次のスニペットでは、内部例外に対して foreach ループを使用しています。

For Each ex In ae.InnerExceptions
    If TypeOf (ex) Is MyCustomException Then
        Console.WriteLine(ex.Message)
    Else
        Throw
    End If
Next
foreach (var e in ae.InnerExceptions)
{
    if (e is MyCustomException)
    {
        Console.WriteLine(e.Message);
    }
    else
    {
        throw;
    }
}

次のスニペットは、Handle() メソッドの使用と同等の機能を示しています。

ae.Handle(Function(ex)
              Return TypeOf (ex) Is MyCustomException
          End Function)
ae.Handle((ex) =>
{
    return ex is MyCustomException;
});

Task.Exception プロパティによる例外の確認

タスクが Faulted 状態で完了した場合、その Exception プロパティを調べることで違反の原因となった例外を見つけることができます。 Exception プロパティを確認する場合は、次の例に示すように、継続元タスクが違反した場合にのみ実行される継続を使用することをお勧めします。

Dim task1 = Task.Factory.StartNew(Sub()
                                      Throw New MyCustomException("task1 faulted.")
                                  End Sub).ContinueWith(Sub(t)
                                                            Console.WriteLine("I have observed a {0}", _
                                                                              t.Exception.InnerException.GetType().Name)
                                                        End Sub,
                                                        TaskContinuationOptions.OnlyOnFaulted)
var task1 = Task.Factory.StartNew(() =>
{
    throw new MyCustomException("Task1 faulted.");
})
.ContinueWith((t) =>
    {
        Console.WriteLine("I have observed a {0}",
            t.Exception.InnerException.GetType().Name);
    },
    TaskContinuationOptions.OnlyOnFaulted);

実際のアプリケーションでは、継続のデリゲートで例外に関する詳細情報を記録して、新しいタスクを作成して例外から回復することが考えられます。

UnobservedTaskException イベント

信頼関係のないプラグインをホストするときなど、シナリオによっては、問題のない例外がよく発生する場合や、難しすぎてすべてを手動で観察できなくなる場合があります。 このような場合、TaskScheduler.UnobservedTaskException イベントを処理できます。 ハンドラーに渡される System.Threading.Tasks.UnobservedTaskExceptionEventArgs インスタンスを使用して、観察されない例外が連結しているスレッドに反映されないようにすることができます。

参照

概念

タスク並列ライブラリ