繁体   English   中英

未使用 await 调用时在异步方法中捕获异常

[英]Catching Exceptions in async methods when not called with await

目标:

我对我在 .Net Core 库中看到的异常行为感到困惑。 这个问题的目标是了解为什么它正在做我所看到的。

执行摘要

我认为当调用async方法时,其中的代码会同步执行,直到遇到第一个 await。 如果是这种情况,那么,如果在该“同步代码”期间抛出异常,为什么它不会传播到调用方法? (就像普通的同步方法一样。)

示例代码:

给定 .Net Core 控制台应用程序中的以下代码:

static void Main(string[] args)
{
    Console.WriteLine("Hello World!");

    try
    {
        NonAwaitedMethod();
    }
    catch (Exception e)
    {
        Console.WriteLine("Exception Caught");
    }

    Console.ReadKey();
}

public static async Task NonAwaitedMethod()
{
    Task startupDone = new Task(() => { });
    var runTask = DoStuff(() =>
    {
        startupDone.Start();
    });
    var didStartup = startupDone.Wait(1000);
    if (!didStartup)
    {
        throw new ApplicationException("Fail One");
    }

    await runTask;
}

public static async Task DoStuff(Action action)
{
    // Simulate starting up blocking
    var blocking = 100000;
    await Task.Delay(500 + blocking);
    action();
    // Do the rest of the stuff...
    await Task.Delay(3000);
}

}

场景:

  1. 当按原样运行时,此代码将抛出异常,但是,除非您在其上设置断点,否则您不会知道它。 Visual Studio 调试器和控制台将给出任何存在问题的指示(除了输出屏幕中的一行注释)。

  2. NonAwaitedMethod的返回类型从Task交换为void 这将导致 Visual Studio 调试器现在因异常而中断。 它也会在控制台中打印出来。 但值得注意的是,在Main找到的catch语句中没有捕获异常。

  3. NonAwaitedMethod的返回类型NonAwaitedMethodvoid ,但取消async 还要更改await runTask;的最后一行await runTask; 运行runTask.Wait(); (这基本上删除了任何异步内容。)运行时,在Main方法的catch语句中捕获异常。

所以,总结一下:

| Scenario   | Caught By Debugger | Caught by Catch |  
|------------|--------------------|-----------------|  
| async Task | No                 | No              |  
| async void | Yes                | No              |  
| void       | N/A                | Yes             |  

问题:

我认为因为异常是在await之前抛出的所以它会同步执行,直到抛出异常为止。

因此我的问题是:为什么场景 1 或 2 都没有被catch语句catch

另外,为什么从Task交换到void返回类型会导致异常被调试器捕获? (即使我没有使用该返回类型。)

在等待完成之前抛出异常,它将同步执行

认为这是相当正确的,但这并不意味着您可以捕获异常。

因为您的代码具有async关键字,它将方法转换为异步状态机,即由特殊类型封装/包装。 任何从异步状态机抛出的异常都会在任务await时被捕获并重新抛出(除了那些async void的)或者它们未被观察到,这可以在TaskScheduler.UnobservedTaskException事件中捕获。

如果从NonAwaitedMethod方法中删除async关键字,则可以捕获异常。

观察这种行为的一个好方法是使用这个:

try
{
    NonAwaitedMethod();

    // You will still see this message in your console despite exception
    // being thrown from the above method synchronously, because the method
    // has been encapsulated into an async state machine by compiler.
    Console.WriteLine("Method Called");
}
catch (Exception e)
{
    Console.WriteLine("Exception Caught");
}

因此,您的代码的编译方式与此类似:

try
{
    var stateMachine = new AsyncStateMachine(() =>
    {
        try
        {
            NonAwaitedMethod();
        }
        catch (Exception ex)
        {
            stateMachine.Exception = ex;
        }
    });

    // This does not throw exception
    stateMachine.Run();
}
catch (Exception e)
{
    Console.WriteLine("Exception Caught");
}

为什么从 Task 交换到 void 返回类型会导致异常被捕获

如果该方法返回Task ,则该任务会捕获异常。

如果该方法为void ,则从任意线程池线程重新抛出异常。 从线程池线程抛出的任何未处理的异常都会导致应用程序崩溃,因此调试器(或 JIT 调试器)可能正在监视此类异常。

如果您想触发并忘记但正确处理异常,您可以使用ContinueWith为任务创建一个延续:

NonAwaitedMethod()
    .ContinueWith(task => task.Exception, TaskContinuationOptions.OnlyOnFaulted);

注意必须访问task.Exception属性才能观察到异常,否则任务调度器仍然会收到UnobservedTaskException事件。

或者,如果需要在Main捕获和处理异常,正确的方法是使用异步 Main 方法

如果在该“同步代码”期间抛出异常,为什么不传播到调用方法? (就像普通的同步方法一样。)

好问题。 事实上, async / await的早期预览版本确实有这种行为。 但是语言团队认为这种行为太令人困惑了。

当你有这样的代码时,很容易理解:

if (test)
  throw new Exception();
await Task.Delay(TaskSpan.FromSeconds(5));

但是这样的代码呢:

await Task.Delay(1);
if (test)
  throw new Exception();
await Task.Delay(TaskSpan.FromSeconds(5));

请记住,如果其可await对象已经完成,则await同步执行。 那么等待从Task.Delay返回的任务时已经过去了 1 毫秒吗? 或者举个更现实的例子,当HttpClient返回本地缓存的响应(同步)时会发生什么? 更一般地,在方法的同步部分直接抛出异常往往会导致代码根据竞争条件改变其语义。

因此,决定单方面改变所有async方法的工作方式,以便所有抛出的异常都放在返回的任务上。 作为一个很好的副作用,这使它们的语义与枚举器块保持一致; 如果您有一个使用yield return的方法,则在实现枚举器之前不会看到任何异常,而不是在调用该方法时。

关于你的场景:

  1. 是的,异常被忽略。 因为Main的代码通过忽略任务来执行“即发即忘”。 “即发即弃”的意思是“我不在乎异常”。 如果您确实关心异常,那么不要使用“即发即忘”; 相反,在某个时候await任务。 任务是async方法如何向调用者报告它们的完成情况,而await是调用代码如何检索任务的结果(并观察异常)
  2. 是的, async void是一个奇怪的怪癖( 一般应该避免)。 它被放入语言中以支持异步事件处理程序,因此它具有类似于事件处理程序的语义。 具体来说,任何逃逸async void方法的异常都会在方法开始时当前的顶级上下文中引发。 这就是异常也适用于 UI 事件处理程序的方式。 在控制台应用程序的情况下,异常会在线程池线程上引发。 普通的async方法返回一个“句柄”,表示异步操作并可以保存异常。 无法捕获async void方法的异常,因为这些方法没有“句柄”。
  3. 嗯,当然。 在这种情况下,该方法是同步的,异常会像往常一样向上移动。

附带说明一下, 永远不要使用Task构造函数 如果要在线程池上运行代码,请使用Task.Run 如果您想要异步委托类型,请使用Func<Task>

async关键字指示编译器应该将方法转换为异步状态机,该状态机在异常处理方面是不可配置的。 如果您希望立即抛出NonAwaitedMethod方法的同步部分异常,除了从方法中删除async关键字之外别无选择。 通过将异步部分移动到异步本地函数中,您可以两全其美:

public static Task NonAwaitedMethod()
{
    Task startupDone = new Task(() => { });
    var runTask = DoStuff(() =>
    {
        startupDone.Start();
    });
    var didStartup = startupDone.Wait(1000);
    if (!didStartup)
    {
        throw new ApplicationException("Fail One");
    }

    return ImlpAsync(); async Task ImlpAsync()
    {
        await runTask;
    };
}

除了使用命名函数,您还可以使用匿名函数:

return ((Func<Task>)(async () =>
{
    await runTask;
}))();

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM