简体   繁体   English

捕获由 async void 方法抛出的异常

[英]Catch an exception thrown by an async void method

Using the async CTP from Microsoft for .NET, is it possible to catch an exception thrown by an async method in the calling method?使用 Microsoft for .NET 的异步 CTP,是否可以捕获调用方法中异步方法抛出的异常?

public async void Foo()
{
    var x = await DoSomethingAsync();

    /* Handle the result, but sometimes an exception might be thrown.
       For example, DoSomethingAsync gets data from the network
       and the data is invalid... a ProtocolException might be thrown. */
}

public void DoFoo()
{
    try
    {
        Foo();
    }
    catch (ProtocolException ex)
    {
          /* The exception will never be caught.
             Instead when in debug mode, VS2010 will warn and continue.
             The deployed the app will simply crash. */
    }
}

So basically I want the exception from the async code to bubble up into my calling code if that is even possible at all.所以基本上,如果可能的话,我希望异步代码中的异常冒泡到我的调用代码中。

It's somewhat weird to read but yes, the exception will bubble up to the calling code - but only if you await or Wait() the call to Foo .读起来有点奇怪,但是是的,异常会冒泡到调用代码——但前提是你awaitWait()调用Foo

public async Task Foo()
{
    var x = await DoSomethingAsync();
}

public async void DoFoo()
{
    try
    {
        await Foo();
    }
    catch (ProtocolException ex)
    {
          // The exception will be caught because you've awaited
          // the call in an async method.
    }
}

//or//

public void DoFoo()
{
    try
    {
        Foo().Wait();
    }
    catch (ProtocolException ex)
    {
          /* The exception will be caught because you've
             waited for the completion of the call. */
    }
} 

As Stephen Cleary wrote in Async/Await - Best Practices in Asynchronous Programming :正如 Stephen Cleary 在Async/Await - 异步编程的最佳实践中所写:

Async void methods have different error-handling semantics. Async void 方法具有不同的错误处理语义。 When an exception is thrown out of an async Task or async Task method, that exception is captured and placed on the Task object.当异常从异步任务或异步任务方法中抛出时,该异常将被捕获并放置在任务对象上。 With async void methods, there is no Task object, so any exceptions thrown out of an async void method will be raised directly on the SynchronizationContext that was active when the async void method started.使用 async void 方法时,没有 Task 对象,因此从 async void 方法抛出的任何异常都将直接在 async void 方法启动时处于活动状态的 SynchronizationContext 上引发。

Note that using Wait() may cause your application to block, if .NET decides to execute your method synchronously.请注意,如果 .NET 决定同步执行您的方法,则使用Wait()可能会导致您的应用程序阻塞。

This explanation http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions is pretty good - it discusses the steps the compiler takes to achieve this magic.这个解释http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions非常好——它讨论了编译器为实现这种魔力所采取的步骤。

The reason the exception is not caught is because the Foo() method has a void return type and so when await is called, it simply returns.未捕获异常的原因是因为 Foo() 方法具有 void 返回类型,因此当调用 await 时,它只是返回。 As DoFoo() is not awaiting the completion of Foo, the exception handler cannot be used.由于 DoFoo() 不等待 Foo 的完成,因此无法使用异常处理程序。

This opens up a simpler solution if you can change the method signatures - alter Foo() so that it returns type Task and then DoFoo() can await Foo() , as in this code:如果您可以更改方法签名,这将打开一个更简单的解决方案 - 更改Foo()以便它返回类型Task然后DoFoo()可以await Foo() ,如以下代码所示:

public async Task Foo() {
    var x = await DoSomethingThatThrows();
}

public async void DoFoo() {
    try {
        await Foo();
    } catch (ProtocolException ex) {
        // This will catch exceptions from DoSomethingThatThrows
    }
}

Your code doesn't do what you might think it does.您的代码并不像您认为的那样工作。 Async methods return immediately after the method begins waiting for the async result.异步方法在方法开始等待异步结果后立即返回。 It's insightful to use tracing in order to investigate how the code is actually behaving.使用跟踪来调查代码的实际行为是很有见地的。

The code below does the following:下面的代码执行以下操作:

  • Create 4 tasks创建 4 个任务
  • Each task will asynchronously increment a number and return the incremented number每个任务都会异步递增一个数字并返回递增后的数字
  • When the async result has arrived it is traced.当异步结果到达时,它被跟踪。

static TypeHashes _type = new TypeHashes(typeof(Program));        
private void Run()
{
    TracerConfig.Reset("debugoutput");

    using (Tracer t = new Tracer(_type, "Run"))
    {
        for (int i = 0; i < 4; i++)
        {
            DoSomeThingAsync(i);
        }
    }
    Application.Run();  // Start window message pump to prevent termination
}


private async void DoSomeThingAsync(int i)
{
    using (Tracer t = new Tracer(_type, "DoSomeThingAsync"))
    {
        t.Info("Hi in DoSomething {0}",i);
        try
        {
            int result = await Calculate(i);
            t.Info("Got async result: {0}", result);
        }
        catch (ArgumentException ex)
        {
            t.Error("Got argument exception: {0}", ex);
        }
    }
}

Task<int> Calculate(int i)
{
    var t = new Task<int>(() =>
    {
        using (Tracer t2 = new Tracer(_type, "Calculate"))
        {
            if( i % 2 == 0 )
                throw new ArgumentException(String.Format("Even argument {0}", i));
            return i++;
        }
    });
    t.Start();
    return t;
}

When you observe the traces当你观察痕迹时

22:25:12.649  02172/02820 {          AsyncTest.Program.Run 
22:25:12.656  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.657  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 0    
22:25:12.658  02172/05220 {          AsyncTest.Program.Calculate    
22:25:12.659  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.659  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 1    
22:25:12.660  02172/02756 {          AsyncTest.Program.Calculate    
22:25:12.662  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.662  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 2    
22:25:12.662  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.662  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 3    
22:25:12.664  02172/02756          } AsyncTest.Program.Calculate Duration 4ms   
22:25:12.666  02172/02820          } AsyncTest.Program.Run Duration 17ms  ---- Run has completed. The async methods are now scheduled on different threads. 
22:25:12.667  02172/02756 Information AsyncTest.Program.DoSomeThingAsync Got async result: 1    
22:25:12.667  02172/02756          } AsyncTest.Program.DoSomeThingAsync Duration 8ms    
22:25:12.667  02172/02756 {          AsyncTest.Program.Calculate    
22:25:12.665  02172/05220 Exception   AsyncTest.Program.Calculate Exception thrown: System.ArgumentException: Even argument 0   
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     
22:25:12.668  02172/02756 Exception   AsyncTest.Program.Calculate Exception thrown: System.ArgumentException: Even argument 2   
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     
22:25:12.724  02172/05220          } AsyncTest.Program.Calculate Duration 66ms      
22:25:12.724  02172/02756          } AsyncTest.Program.Calculate Duration 57ms      
22:25:12.725  02172/05220 Error       AsyncTest.Program.DoSomeThingAsync Got argument exception: System.ArgumentException: Even argument 0  

Server stack trace:     
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     

Exception rethrown at [0]:      
   at System.Runtime.CompilerServices.TaskAwaiter.EndAwait()    
   at System.Runtime.CompilerServices.TaskAwaiter`1.EndAwait()  
   at AsyncTest.Program.DoSomeThingAsyncd__8.MoveNext() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 106    
22:25:12.725  02172/02756 Error       AsyncTest.Program.DoSomeThingAsync Got argument exception: System.ArgumentException: Even argument 2  

Server stack trace:     
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     

Exception rethrown at [0]:      
   at System.Runtime.CompilerServices.TaskAwaiter.EndAwait()    
   at System.Runtime.CompilerServices.TaskAwaiter`1.EndAwait()  
   at AsyncTest.Program.DoSomeThingAsyncd__8.MoveNext() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 0      
22:25:12.726  02172/05220          } AsyncTest.Program.DoSomeThingAsync Duration 70ms   
22:25:12.726  02172/02756          } AsyncTest.Program.DoSomeThingAsync Duration 64ms   
22:25:12.726  02172/05220 {          AsyncTest.Program.Calculate    
22:25:12.726  02172/05220          } AsyncTest.Program.Calculate Duration 0ms   
22:25:12.726  02172/05220 Information AsyncTest.Program.DoSomeThingAsync Got async result: 3    
22:25:12.726  02172/05220          } AsyncTest.Program.DoSomeThingAsync Duration 64ms   

You will notice that the Run method completes on thread 2820 while only one child thread has finished (2756).您会注意到 Run 方法在线程 2820 上完成,而只有一个子线程已完成 (2756)。 If you put a try/catch around your await method you can "catch" the exception in the usual way although your code is executed on another thread when the calculation task has finished and your contiuation is executed.如果你在 await 方法周围放置一个 try/catch,你可以用通常的方式“捕获”异常,尽管当计算任务完成并且你的 contiuation 被执行时,你的代码是在另一个线程上执行的。

The calculation method traces the thrown exception automatically because I did use the ApiChange.Api.dll from the ApiChange tool.计算方法会自动跟踪抛出的异常,因为我确实使用了ApiChange工具中的 ApiChange.Api.dll。 Tracing and Reflector helps a lot to understand what is going on. Tracing 和 Reflector 对理解正在发生的事情有很大帮助。 To get rid of threading you can create your own versions of GetAwaiter BeginAwait and EndAwait and wrap not a task but eg a Lazy and trace inside your own extension methods.要摆脱线程,您可以创建自己的 GetAwaiter BeginAwait 和 EndAwait 版本,而不是包装任务,而是例如 Lazy 并在您自己的扩展方法中进行跟踪。 Then you will get much better understanding what the compiler and what the TPL does.然后您将更好地理解编译器和 TPL 的作用。

Now you see that there is no way to get in a try/catch your exception back since there is no stack frame left for any exception to propagate from.现在您看到没有办法返回 try/catch 异常,因为没有堆栈框架可供任何异常从中传播。 Your code might be doing something totally different after you did initiate the async operations.在您启动异步操作后,您的代码可能会做一些完全不同的事情。 It might call Thread.Sleep or even terminate.它可能会调用 Thread.Sleep 甚至终止。 As long as there is one foreground thread left your application will happily continue to execute asynchronous tasks.只要还剩下一个前台线程,您的应用程序就会愉快地继续执行异步任务。


You can handle the exception inside the async method after your asynchronous operation did finish and call back into the UI thread.在异步操作完成并回调到 UI 线程后,您可以在异步方法中处理异常。 The recommended way to do this is with TaskScheduler.FromSynchronizationContext .推荐的方法是使用TaskScheduler.FromSynchronizationContext That does only work if you have an UI thread and it is not very busy with other things.只有当你有一个 UI 线程并且它不是很忙于其他事情时才有效。

Its also important to note that you will lose the chronological stack trace of the exception if you you have a void return type on an async method.同样重要的是要注意,如果异步方法的返回类型为 void,您将丢失异常的时间顺序堆栈跟踪。 I would recommend returning Task as follows.我建议按如下方式返回 Task。 Going to make debugging a whole lot easier.将使调试变得更加容易。

public async Task DoFoo()
    {
        try
        {
            return await Foo();
        }
        catch (ProtocolException ex)
        {
            /* Exception with chronological stack trace */     
        }
    }

This blog explains your problem neatly Async Best Practices .该博客巧妙地解释了您的问题Async Best Practices

The gist of it being you shouldn't use void as return for an async method, unless it's an async event handler, this is bad practice because it doesn't allow exceptions to be caught;-).它的要点是你不应该使用 void 作为异步方法的返回值,除非它是一个异步事件处理程序,这是不好的做法,因为它不允许捕获异常;-)。

Best practice would be to change the return type to Task.最佳做法是将返回类型更改为任务。 Also, try to code async all the way trough, make every async method call and be called from async methods.另外,尝试一直编写异步代码,进行每个异步方法调用并从异步方法中调用。 Except for a Main method in a console, which can't be async (before C# 7.1).除了控制台中的 Main 方法,它不能是异步的(在 C# 7.1 之前)。

You will run into deadlocks with GUI and ASP.NET applications if you ignore this best practice.如果忽略此最佳实践,您将遇到 GUI 和 ASP.NET 应用程序的死锁。 The deadlock occurs because these applications runs on a context that allows only one thread and won't relinquish it to the async thread.发生死锁是因为这些应用程序运行在只允许一个线程并且不会将其交给异步线程的上下文中。 This means the GUI waits synchronously for a return, while the async method waits for the context: deadlock.这意味着 GUI 同步等待返回,而异步方法等待上下文:死锁。

This behaviour won't happen in a console application, because it runs on context with a thread pool.这种行为不会发生在控制台应用程序中,因为它在具有线程池的上下文中运行。 The async method will return on another thread which will be scheduled. async 方法将在另一个将被调度的线程上返回。 This is why a test console app will work, but the same calls will deadlock in other applications...这就是为什么测试控制台应用程序可以工作,但相同的调用会在其他应用程序中死锁......

The exception can be caught in the async function.可以在异步函数中捕获异常。

public async void Foo()
{
    try
    {
        var x = await DoSomethingAsync();
        /* Handle the result, but sometimes an exception might be thrown
           For example, DoSomethingAsync get's data from the network
           and the data is invalid... a ProtocolException might be thrown */
    }
    catch (ProtocolException ex)
    {
          /* The exception will be caught here */
    }
}

public void DoFoo()
{
    Foo();
}

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

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