未使用 await 调用时在异步方法中捕获异常 [英] Catching Exceptions in async methods when not called with await

查看:33
本文介绍了未使用 await 调用时在异步方法中捕获异常的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

目标:

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

执行摘要

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

示例代码:

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

static void Main(string[] args){Console.WriteLine("Hello World!");尝试{NonAwaitedMethod();}捕获(例外 e){Console.WriteLine("异常捕获");}Console.ReadKey();}公共静态异步任务 NonAwaitedMethod(){任务启动完成 = new Task(() => { });var runTask = DoStuff(() =>{启动完成.开始();});var didStartup = startupDone.Wait(1000);如果(!didStartup){throw new ApplicationException("失败一");}等待运行任务;}公共静态异步任务 DoStuff(Action action){//模拟启动阻塞var 阻塞 = 100000;等待 Task.Delay(500 + 阻塞);行动();//做剩下的事情...等待 Task.Delay(3000);}

}

场景:

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

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

  3. 保留NonAwaitedMethod的返回类型为void,但去掉async.还要将最后一行从 await runTask; 更改为 runTask.Wait(); (这实际上删除了任何异步内容.)运行时,异常会在 Main 方法中的 >catch 语句.

总结一下:

<代码>|情景 |被调试器捕获 |被捕获 ||------------|------------|----------------||异步任务 |没有 |没有 ||异步无效|是 |没有 ||无效 |不适用 |是 |

问题:

我认为因为 异常是在 await 完成之前抛出的,所以 它会同步执行直到,并通过抛出的例外.

因此我的问题是:为什么 catch 语句不会捕获场景 1 或场景 2?

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

解决方案

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

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

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

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

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

试试{NonAwaitedMethod();//尽管有异常,您仍会在控制台中看到此消息//被上面的方法同步抛出,因为方法//已经被编译器封装成异步状态机.Console.WriteLine("方法调用");}捕获(例外 e){Console.WriteLine("异常捕获");}

所以你的代码是这样编译的:

试试{var stateMachine = new AsyncStateMachine(() =>{尝试{NonAwaitedMethod();}捕获(异常前){stateMachine.Exception = ex;}});//这不会抛出异常stateMachine.Run();}捕获(例外 e){Console.WriteLine("异常捕获");}

<块引用>

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

如果方法返回一个Task,异常就会被任务捕获.

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

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

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

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

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

Goal:

I am confused by the behavior I am seeing with exceptions in my .Net Core library. The goal of this question is to understand why it is doing what I am seeing.

Executive Summary

I thought that when an async method is called, the code in it is executed synchronously until it hits the first await. If that is the case, then, if an exception is thrown during that "synchronous code", why is it not propagated up to the calling method? (As a normal synchronous method would do.)

Example Code:

Given the following code in a .Net Core Console Application:

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

}

Scenarios:

  1. When run as is, this code will throw an exception, but, unless you have a break point on it, you will not know it. The Visual Studio Debugger nor the Console will give any indication that there was an issue (aside from a one line note in the Output screen).

  2. Swap the return type of NonAwaitedMethod from Task to void. This will cause the Visual Studio Debugger to now break on the exception. It will also be printed out in the console. But notably, the exception is NOT caught in the catch statement found in Main.

  3. Leave the return type of NonAwaitedMethod as void, but take off the async. Also change the last line from await runTask; to runTask.Wait(); (This essentially removes any async stuff.) When run, the exception is caught in the catch statement in the Main method.

So, to summarize:

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

Questions:

I thought that because the exception was thrown before an await was done, that it would execute synchronously up to, and through the throwing of the exception.

Hence my question of: Why does neither scenario 1 or 2 get caught by the catch statement?

Also, why does swapping from Task to void return type cause the exception to get caught by the Debugger? (Even though I am not using that return type.)

解决方案

exception was thrown before an await was done, that it would execute synchronously

Thought this is fairly true, but it doesn't mean you could catch the exception.

Because your code has async keyword, which turns the method into an async state machine i.e. encapsulated / wrapped by a special type. Any exception thrown from async state machine will get caught and re-thrown when the task is awaited (except for those async void ones) or they go unobserved, which can be caught in TaskScheduler.UnobservedTaskException event.

If you remove async keyword from the NonAwaitedMethod method, you can catch the exception.

A good way to observe this behavior is using this:

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");
}

So your code is compiled similarly to this:

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");
}

why does swapping from Task to void return type cause the exception to get caught

If the method returns a Task, the exception is caught by the task.

If the method is void, then the exception gets re-thrown from an arbitrary thread pool thread. Any unhandled exception thrown from thread pool thread will cause the app to crash, so chances are the debugger (or maybe the JIT debugger) is watching this sort of exceptions.

If you want to fire and forget but properly handle the exception, you could use ContinueWith to create a continuation for the task:

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

Note you have to visit task.Exception property to make the exception observed, otherwise, task scheduler still will receive UnobservedTaskException event.

Or if the exception needs to be caught and processed in Main, the correct way to do that is using async Main methods.

这篇关于未使用 await 调用时在异步方法中捕获异常的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆