9

使用 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
        });

更推荐使用哪种方法?各自的优缺点是什么?

4

5 回答 5

3

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

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

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

处理延续中的异常

  • 当异常清理应该由不同的调度时TaskScheduler,例如在线程池上运行“主要”任务但将所有异常记录编组到 UI 线程。
  • 如果有多个延续是有意义的,每个延续都做不同的事情,除了例外,尽管这有点不寻常。
  • 为了确保Task您未提供代码的 s 的异常得到适当的观察和处理,例如在TaskFactory.FromAsync. 虽然视情况而定,但也可以通过等待Task.
于 2013-02-13T14:50:27.187 回答
3

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

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

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

于 2013-02-13T16:04:32.910 回答
1

在某种程度上,这是一个偏好问题,特别是如果您“拥有”任务代码和调用代码。这里有一些要考虑的事情。

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

还要注意.NET 4.5中关于未捕获异常的行为变化。这种变化有效地从“纯粹主义”方法(在未捕获的任务异常上拆除过程)到不那么严厉的方法。尽管如此,故意依赖新行为还是不好的。

至于你的两种选择中的哪一种,选择第二个有一个论点:继续处理异常。 在 .NET 中返回 .NET 的方法将越来越普遍Task。例如,Stream.ReadAsync。要正确使用这些方法,您需要一个延续(传统方式,或者使用带有新await功能的 try/catch 块,这相当于同一件事,但更容易编码和阅读)。因此,最好养成假设任何Task可能失败的习惯,除非您明确知道其他情况,并编写适当的异常处理行为。

如果您有兴趣,这里是在 .NET 4.5 中编写第二个示例的另一种方法。

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

另一个差异最常适用于从 UI 线程调用代码的Windows 窗体或 WPF 应用程序。这里使用时 TPL 的默认行为await是使用将它们编组回 UI 线程的同步上下文来运行延续。也就是说,如果你Task.Run是从 UI 线程调用的,延续也将在 UI 线程上运行。

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

于 2013-02-13T14:03:02.170 回答
1

你的两个例子在概念上是不同的。

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

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

猜猜答案是否完全取决于您要达到的目标-没有明确的答案-但第二个更符合 tpl。

此外,在第二个示例中,task.IsFaulted 比 task.Exception 更清晰

于 2013-02-13T14:44:43.373 回答
-1

我会说这取决于上下文。正如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()我们没有的情况下,所以我们将它传播给调用者,说明我们处于不一致的状态。

于 2013-02-13T14:31:07.837 回答