异常处理(任务并行库)

由在任务内部运行的用户代码引发的未处理异常将传播回联接线程,但在本主题中后面描述的某些情况下除外。 当使用某个静态或实例 Task.WaitTask<TResult>.Wait 方法时,会传播异常,您可通过将调用包括在 try-catch 语句中来处理这些异常。 如果任务是所附加子任务的父级,或者您在等待多个任务,则可能会引发多个异常。 为了将所有异常传播回调用线程,任务基础结构会将这些异常包装在 AggregateException 实例中。 AggregateException 具有一个 InnerExceptions 属性,可枚举该属性来检查引发的所有原始异常,并单独处理(或不处理)每个异常。 即使只引发了一个异常,也仍会将该异常包装在 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>.WaitWaitAnyWaitAll 方法处捕获的 AggregateException 的 AggregateException().InnerExceptions 属性包含一个或多个 AggregateException 实例,而不是导致错误的原始异常。 为了避免必须循环访问嵌套的 AggregateExceptions 的情况,可使用 Flatten() 方法移除所有嵌套的 AggregateExceptions,以便 AggregateException() InnerExceptions 属性包含原始异常。 在下面的示例中,嵌套的 AggregateException 实例已单一化,并且仅在一个循环中处理。

' 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() 属性,或者任何其他允许您确定异常是否为良性的异常相关信息。 在 Handle() 返回之后,将会立即在新的 AggregateException 实例中重新引发委托为其返回 false 的任何异常。

下面的代码段对内部异常使用 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 实例可用于阻止未观察到的异常传播回到联接线程。

请参见

概念

任务并行库