TPL的异常处理

本文关键字:异常处理 TPL | 更新日期: 2023-09-27 17:58:53

使用TPL/Tasks,我可以使用内部try/catch语句执行异常处理:

  Task.Factory.StartNew(
    ()=>
      {
        try
        {
          // Do stuff
        }
        catch
        {
          // Handle exception
        }
      });

或者使用ContinueWith,就像这样:

Task.Factory.StartNew(
    ()=>
      {
          // Do stuff
      }).ContinueWith(
        task =>
        {
          if(task.Exception != null)
            // Handle exception
        });

更建议使用哪种方法?每种方法的利弊是什么?

TPL的异常处理

如果你能够在任务本身抛出的方法中正确处理异常,你应该在第一个任务中捕获它,而不是在延续中,除非你有一些令人信服的理由不这样做。在与任务本身相同的范围内创建延续(就像第二个例子中所做的那样)会不必要地增加更多的工作。

当从与定义任务的范围完全不同的范围处理异常时,在延续中处理异常既有用又必要。例如,如果您有一个方法被赋予了某个任意任务,并且它不知道该任务的定义可能是什么,但它需要在代码引发异常的情况下做一些事情,那么您需要有一个处理异常的延续。

请注意,如果将有一个处理异常的延续,则可以使用TaskContinuationOptions.OnlyOnFaulted仅在任务引发异常时运行延续,而不是在延续的定义中进行检查。

这在很大程度上取决于您的设计需求。需要考虑的一些事项:

在抛出异常的任务中捕获异常

  • 当一个任务表示某个不可分割的工作单元,包括在特定异常类型之后进行清理时
  • 当特定的异常类型由于某种原因不应该在任务之外传播时,例如,它需要封装在不同类型的外部异常中,以满足客户端代码对契约的期望

在延续中处理异常

  • 当异常清理应由不同的TaskScheduler调度时,例如在线程池上运行"主要"任务,但将所有异常日志整理到UI线程上
  • 如果有意义的话,让多个continuation每个都做不同的事情,但有一个例外,尽管这有点不寻常
  • 为了确保未提供代码的Task的异常得到适当的观察和处理,例如在TaskFactory.FromAsync创建的任务之后进行适当的清理。尽管这取决于具体情况,但也可以通过等待Task来完成

您的两个示例在概念上不同。

第一个在执行任务内部处理异常。catch之后运行的任何代码仍将被执行。

第二个调度另一个异步任务,该任务将始终由调度程序在第一个任务完成后运行。

我猜答案是,这取决于你想要实现的目标——没有明确的答案——但第二个答案更符合tpl。

此外,在第二个示例中,task。IsFaulted更清楚这项任务。异常

在某种程度上,这是一个偏好问题,尤其是如果您"拥有"任务代码和调用代码。以下是一些需要考虑的事项。

首先,您应该只捕获您知道如何处理的异常。无论您是用continuation还是用操作中的try/catch来处理它们,这都适用。

还请注意中更改的行为。NET 4.5关于未捕获的异常。这一变化从"纯粹主义"方法(在未捕获的任务异常上拆除流程)有效地转变为不那么严厉的方法。不过,故意依赖这种新行为是不好的。

至于你的两个选择中的哪一个,有一个选择第二个的论点:在延续中处理异常它将在中越来越常见。NET的方法来返回Task。例如,Stream。ReadAsync。要正确使用这些方法,您需要一个延续(可以是传统的方式,也可以使用具有新await功能的try/catch块,这相当于相同的东西,但更容易编码和读取)。因此,养成这样一个习惯,即假设任何Task都可能失败,除非您明确知道其他情况,并编码适当的异常处理行为。

如果您感兴趣,这里有一种在中对第二个示例进行编码的替代方法。净4.5。

async Task MyMethod()
{
    try
    {
        await Task.Run(
            () =>
            {
                // Some work.
            });
    }
    catch (SomeException ex)
    {
    }
}

另一个差异最常见于Windows窗体或WPF应用程序,其中代码是从UI线程调用的。这里,当使用await时,TPL的默认行为是使用同步上下文运行延续,该同步上下文将它们封送回UI线程。也就是说,如果从UI线程调用Task.Run,那么continuation也将在UI线程上运行。

如果您希望向用户显示对话框以响应异常,这将非常有用。在Task工作负载中,您将无法成功地做到这一点。当使用显式continuations而不是await时,必须传递使用TaskScheduler创建的TaskScheduler。从CurrentSynchronizationContext到ContinueWith的适当重载。

我认为这取决于上下文。正如Olly所说,您应该只处理您知道如何处理的异常。我想说如果你知道如何处理异常,你应该处理它。

一个例子是,如果您有一个任务应该从文件中加载一些数据,或者回退到某个默认值(这可能引发异常),那么一种方法是(伪代码):

Task.Factory.StartNew(()=>
{
    MyObject objectToSet = null;
    try
    {
        objectToSet = File.Open("mydata");
    }
    catch (FileException ex)
    {
        // this will catch the FileException because we know how to handle that!
        // the following will however throw an exception that we cannot handle
        objectToSet = GetFallBackValue(); 
    }
    // when we are here we promise that the objectToSet is valid.
});

File.Open的情况下,我们知道如何继续。在GetFallBackValue()的情况下,我们没有,所以我们将其传播给调用者,声明我们处于不一致的状态。