简体   繁体   English

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

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

Goal:目标:

I am confused by the behavior I am seeing with exceptions in my .Net Core library.我对我在 .Net Core 库中看到的异常行为感到困惑。 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.我认为当调用async方法时,其中的代码会同步执行,直到遇到第一个 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:给定 .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);
}

} }

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). Visual Studio 调试器和控制台将给出任何存在问题的指示(除了输出屏幕中的一行注释)。

  2. Swap the return type of NonAwaitedMethod from Task to void .NonAwaitedMethod的返回类型从Task交换为void This will cause the Visual Studio Debugger to now break on the exception.这将导致 Visual Studio 调试器现在因异常而中断。 It will also be printed out in the console.它也会在控制台中打印出来。 But notably, the exception is NOT caught in the catch statement found in Main .但值得注意的是,在Main找到的catch语句中没有捕获异常。

  3. Leave the return type of NonAwaitedMethod as void , but take off the async .NonAwaitedMethod的返回类型NonAwaitedMethodvoid ,但取消async Also change the last line from await runTask;还要更改await runTask;的最后一行await runTask; to runTask.Wait();运行runTask.Wait(); (This essentially removes any async stuff.) When run, the exception is caught in the catch statement in the Main method. (这基本上删除了任何异步内容。)运行时,在Main方法的catch语句中捕获异常。

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.我认为因为异常是在await之前抛出的所以它会同步执行,直到抛出异常为止。

Hence my question of: Why does neither scenario 1 or 2 get caught by the catch statement?因此我的问题是:为什么场景 1 或 2 都没有被catch语句catch

Also, why does swapping from Task to void return type cause the exception to get caught by the Debugger?另外,为什么从Task交换到void返回类型会导致异常被调试器捕获? (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 ie encapsulated / wrapped by a special type.因为您的代码具有async关键字,它将方法转换为异步状态机,即由特殊类型封装/包装。 Any exception thrown from async state machine will get caught and re-thrown when the task is await ed (except for those async void ones) or they go unobserved, which can be caught in TaskScheduler.UnobservedTaskException event.任何从异步状态机抛出的异常都会在任务await时被捕获并重新抛出(除了那些async void的)或者它们未被观察到,这可以在TaskScheduler.UnobservedTaskException事件中捕获。

If you remove async keyword from the NonAwaitedMethod method, you can catch the exception.如果从NonAwaitedMethod方法中删除async关键字,则可以捕获异常。

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为什么从 Task 交换到 void 返回类型会导致异常被捕获

If the method returns a Task , the exception is caught by the task.如果该方法返回Task ,则该任务会捕获异常。

If the method is void , then the exception gets re-thrown from an arbitrary thread pool thread.如果该方法为void ,则从任意线程池线程重新抛出异常。 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.从线程池线程抛出的任何未处理的异常都会导致应用程序崩溃,因此调试器(或 JIT 调试器)可能正在监视此类异常。

If you want to fire and forget but properly handle the exception, you could use ContinueWith to create a continuation for the task:如果您想触发并忘记但正确处理异常,您可以使用ContinueWith为任务创建一个延续:

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

Or if the exception needs to be caught and processed in Main , the correct way to do that is using async Main methods .或者,如果需要在Main捕获和处理异常,正确的方法是使用异步 Main 方法

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.) (就像普通的同步方法一样。)

Good question.好问题。 And in fact, the early preview versions of async / await did have that behavior.事实上, async / await的早期预览版本确实有这种行为。 But the language team decided that behavior was just too confusing.但是语言团队认为这种行为太令人困惑了。

It's easy enough to understand when you have code like this:当你有这样的代码时,很容易理解:

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

But what about code like this:但是这样的代码呢:

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

Remember that await acts synchronously if its awaitable is already completed.请记住,如果其可await对象已经完成,则await同步执行。 So has 1 millisecond gone by by the time the task returned from Task.Delay is awaited?那么等待从Task.Delay返回的任务时已经过去了 1 毫秒吗? Or for a more realistic example, what happens when HttpClient returns a locally cached response (synchronously)?或者举个更现实的例子,当HttpClient返回本地缓存的响应(同步)时会发生什么? More generally, the direct throwing of exceptions during the synchronous part of the method tends to result in code that changes its semantics based on race conditions.更一般地,在方法的同步部分直接抛出异常往往会导致代码根据竞争条件改变其语义。

So, the decision was made to unilaterally change the way all async methods work so that all exceptions thrown are placed on the returned task.因此,决定单方面改变所有async方法的工作方式,以便所有抛出的异常都放在返回的任务上。 As a nice side effect, this brings their semantics in line with enumerator blocks;作为一个很好的副作用,这使它们的语义与枚举器块保持一致; if you have a method that uses yield return , any exceptions will not be seen until the enumerator is realized , not when the method is called .如果您有一个使用yield return的方法,则在实现枚举器之前不会看到任何异常,而不是在调用该方法时。

Regarding your scenarios:关于你的场景:

  1. Yes, the exception is ignored.是的,异常被忽略。 Because the code in Main is doing "fire and forget" by ignoring the task.因为Main的代码通过忽略任务来执行“即发即忘”。 And "fire and forget" means "I don't care about exceptions". “即发即弃”的意思是“我不在乎异常”。 If you do care about exceptions, then don't use "fire and forget";如果您确实关心异常,那么不要使用“即发即忘”; instead, await the task at some point.相反,在某个时候await任务。 The task is how async methods report their completion to their callers, and doing an await is how calling code retrieves the results of the task (and observe exceptions) . 任务是async方法如何向调用者报告它们的完成情况,而await是调用代码如何检索任务的结果(并观察异常)
  2. Yes, async void is an odd quirk (and should be avoided in general ).是的, async void是一个奇怪的怪癖( 一般应该避免)。 It was put in the language to support asynchronous event handlers, so it has semantics that are similar to event handlers.它被放入语言中以支持异步事件处理程序,因此它具有类似于事件处理程序的语义。 Specifically, any exceptions that escape the async void method are raised on the top-level context that was current at the beginning of the method.具体来说,任何逃逸async void方法的异常都会在方法开始时当前的顶级上下文中引发。 This is how exceptions also work for UI event handlers.这就是异常也适用于 UI 事件处理程序的方式。 In the case of a console application, exceptions are raised on a thread pool thread.在控制台应用程序的情况下,异常会在线程池线程上引发。 Normal async methods return a "handle" that represents the asynchronous operation and can hold exceptions.普通的async方法返回一个“句柄”,表示异步操作并可以保存异常。 Exceptions from async void methods cannot be caught, since there is no "handle" for those methods.无法捕获async void方法的异常,因为这些方法没有“句柄”。
  3. Well, of course.嗯,当然。 In this case the method is synchronous, and exceptions travel up the stack just like normal.在这种情况下,该方法是同步的,异常会像往常一样向上移动。

On a side note, never, ever use the Task constructor .附带说明一下, 永远不要使用Task构造函数 If you want to run code on the thread pool, use Task.Run .如果要在线程池上运行代码,请使用Task.Run If you want to have an asynchronous delegate type, use Func<Task> .如果您想要异步委托类型,请使用Func<Task>

The async keyword indicates that the compiler should transform the method to an async state machine, which is not configurable regarding the handling of the exceptions. async关键字指示编译器应该将方法转换为异步状态机,该状态机在异常处理方面是不可配置的。 If you want the sync-part-exceptions of the NonAwaitedMethod method to be thrown immediately, there is no other option than removing the async keyword from the method.如果您希望立即抛出NonAwaitedMethod方法的同步部分异常,除了从方法中删除async关键字之外别无选择。 You can have the best of both worlds by moving the async part into an async local function :通过将异步部分移动到异步本地函数中,您可以两全其美:

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

Instead of using a named function, you could also use an anonymous one:除了使用命名函数,您还可以使用匿名函数:

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

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

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