如果未等待异步任务,它会在哪里抛出异常?

14
我有以下示例:(请阅读代码中的注释,因为这将更有意义)
public async Task<Task<Result>> MyAsyncMethod() 
{
    Task<Result> resultTask = await _mySender.PostAsync();
    return resultTask; 

    // in real-life case this returns to a different assembly which I can't change
   // but I need to do some exception handling on the Result in here
}

假设_mySenderPostAsync方法如下:

public Task<Task<Result>> PostAsync() 
{
  Task<Result> result = GetSomeTask(); 
  return result;
}

问题是:

由于我在MyAsyncMethod中不等待实际的Result,如果PostAsync方法抛出异常,异常会在哪个上下文中被抛出和处理?

以及

我是否有办法在我的程序集中处理异常?

当我尝试将MyAsyncMethod更改为以下内容时,我感到惊讶:

 public async Task<Task<Result>> MyAsyncMethod() 
{
  try 
  {
    Task<Result> resultTask = await _mySender.PostAsync();
    return resultTask; 
  }
  catch (MyCustomException ex) 
  {
  } 
}

即使没有等待实际结果,异常在这里被捕获。发生这种情况是因为 PostAsync 的结果已经可用,并且异常是在此上下文中抛出的,对吗?

是否可以使用 ContinueWith 处理当前类中的异常?例如:

public async Task<Task<Result>> MyAsyncMethod() 
{
    Task<Result> resultTask = await _mySender.PostAsync();
    var exceptionHandlingTask = resultTask.ContinueWith(t => { handle(t.Exception)}, TaskContinuationOptions.OnlyOnFaulted);
    return resultTask;
}

您可能希望查看 TPL 的 异常处理 页面。 - Dan Lyons
3个回答

22

这是很多问题集中在一个“问题”中,但没关系...

如果不等待异步任务,它会在哪里抛出异常?

未观察到的任务异常将由TaskScheduler.UnobservedTaskException事件引发。此事件被“最终”引发,因为必须实际垃圾回收任务,才会考虑其异常未处理。

如果我不等待 MyAsyncMethod 中实际结果,而 PostAsync 方法抛出异常,那么异常会在哪个上下文中抛出和处理?

任何使用async修饰符并返回Task的方法都会将所有异常放置在返回的Task上。

是否有办法在我的程序集中处理异常?

是的,你可以替换返回的任务,例如:

async Task<Result> HandleExceptionsAsync(Task<Result> original)
{
  try
  {
    return await original;
  }
  catch ...
}

public async Task<Task<Result>> MyAsyncMethod()
{
  Task<Result> resultTask = await _mySender.PostAsync();
  return HandleExceptionsAsync(resultTask);
}

我惊讶地发现,当我试图将MyAsyncMethod更改为[同步返回内部任务]时,即使实际结果没有等待,异常也被捕获在这里。

实际上,这意味着您调用的方法不是async Task,正如您的代码示例所显示的那样。它是一个非async、返回Task的方法,当这些方法之一引发异常时,它会像任何其他异常一样被处理(即,它会直接通过调用堆栈传递;不会放置在返回的Task上)。

在当前类中使用ContinueWith处理异常是否可行?

可以,但await更清晰。


嗨,Stephen,感谢您的回答。您是正确的,示例是不正确的,该方法是一个非异步返回的Task。我将尝试您的建议,谢谢。 - Dan Dinu
嘿@Dan Dinu,我知道已经有一段时间了,但是为什么你不接受这个很好的答案呢? - Jony Adamit

6

我使用一个扩展方法来处理 Task 的通用错误。这提供了一种记录所有错误并在发生错误时执行自定义操作的方式。

public static async void ErrorHandle(this Task task, Action action = null)
{
    try
    {
        await task.ConfigureAwait(false);
    }
    catch (Exception e)
    {
        Log.Error(e);
        if (action != null)
            action();
    }
}

我倾向于在进行“发射并忘记”的任务时使用它:

Task.Run(() => ProcessData(token), token).ErrorHandle(OnError);

1
这并没有回答所提出的问题。此外,使用TPL的主要原因之一是它在错误处理方面具有很强的功能性。为什么你要费力地回到回调式的错误处理方式呢? - Servy
我可以看到它用于“fire and forget”操作。例如,当我执行一堆可以并行运行的任务时,我不想等待每个任务完成,但仍希望尽快抛出异常(即用于日志记录)。 - David Deutsch

5
由于在 MyAsyncMethod 中没有等待实际结果,如果 PostAsync 方法抛出异常,那么异常将在哪个上下文中被抛出和处理?
如果您的代码中没有等待任何任务或分配延续,则行为可能因您使用的 .NET 框架版本而异。在两种情况下,返回的 Task 将吞噬异常,区别将在最终化时发生:
1. .NET 4.0 - 终结器线程将重新引发被吞噬的异常。如果未注册全局异常处理程序,则会终止进程。 2. .NET 4.5 及以上版本 - 异常将被吞噬并不会被注意到。
在两种情况下 TaskScheduler.UnobservedTaskException 事件都将触发:
当故障任务的未观察到的异常即将触发异常升级策略时,该事件会发生,默认情况下,这会终止进程。
当非异步任务返回方法同步执行时,异常会立即传播,这就是为什么您在不使用await的情况下捕获异常的原因,但您绝对不应该在您的代码中依赖于它。
“我能否在我的程序集中处理异常?”
是的,您可以。我建议您等待在程序集内部执行的任务。
如果您没有等待任何内容,那么就没有理由使用async修饰符:
public Task<Result> PostAsync() 
{
     return GetSomeTask();
}

然后,您可以在PostAsync上使用await并在那里捕获异常:
public async Task<Result> MyAsyncMethod() 
{
    try
    {
         // No need to use Task<Result> as await will unwrap the outter task
         return await _mySender.PostAsync();
     }
     catch (MyCustomException e)
     {
           // handle here
     }
}

你甚至可以进一步修改这段代码,删除async关键字,并且可能在调用MyAsyncMethod的方法的调用栈更高处捕获异常。

1
如果在异步方法中抛出异常,即使在第一个await之前,它仍将被包装在返回的Task中。这不会导致实际方法调用抛出异常,即使它在同步运行时达到了该异常。 - Servy
没错,那是一个非异步返回Task的方法。我已经修改了。 - Yuval Itzchakov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接