捕获异步 void 方法引发的异常

Posted

技术标签:

【中文标题】捕获异步 void 方法引发的异常【英文标题】:Catch an exception thrown by an async void method 【发布时间】:2011-07-19 23:48:09 【问题描述】:

使用 Microsoft 为 .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. */
    

所以基本上我希望异步代码中的异常冒泡到我的调用代码中 如果这是可能的话。

【问题讨论】:

这对您有帮助吗? social.msdn.microsoft.com/Forums/en/async/thread/… 如果将来有人偶然发现这一点,Async/Await Best Practices... article 在“图 2 无法用 Catch 捕获异步 Void 方法的异常”中对此进行了很好的解释。 "当异步任务或异步任务 方法抛出异常时,该异常被捕获并放置在任务对象上。使用异步 void 方法,没有任务对象,任何异常抛出async void 方法将直接在异步 void 方法启动时处于活动状态的 SynchronizationContext 上引发。" 你可以使用this approach或this 【参考方案1】:

读起来有点奇怪,但是是的,异常会冒泡到调用代码 - 但只有 如果你 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. */
    
 

正如 Stephen Cleary 在Async/Await - Best Practices in Asynchronous Programming 中所写:

Async void 方法具有不同的错误处理语义。当异步任务或异步任务方法抛出异常时,会捕获该异常并将其放置在任务对象上。对于 async void 方法,没有 Task 对象,因此任何从 async void 方法抛出的异常都将直接在 async void 方法启动时处于活动状态的 SynchronizationContext 上引发。

请注意,如果 .NET 决定同步执行您的方法,使用 Wait() 可能会导致您的应用程序阻塞。

http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions 这个解释非常好 - 它讨论了编译器为实现这一魔力所采取的步骤。

【讨论】:

我的意思是直接阅读 - 而我知道实际发生的事情非常复杂 - 所以我的大脑告诉我不要相信我的眼睛...... 我认为 Foo() 方法应该标记为 Task 而不是 void。 我很确定这会产生 AggregateException。因此,此答案中出现的 catch 块不会捕获异常。 "但仅当您 await 或 Wait() 调用 Foo" 当 Foo 返回 void 时,您如何 await 调用 Foo? async void Foo()Type void is not awaitable? 不能等待 void 方法,可以吗?【参考方案2】:

这个博客巧妙地解释了你的问题Async Best Practices。

你不应该使用 void 作为异步方法的返回值,除非它是一个异步事件处理程序,这是一种不好的做法,因为它不允许捕获异常 ;-)。

最佳做法是将返回类型更改为任务。 此外,尝试一直编写异步代码,进行每个异步方法调用并从异步方法中调用。除了控制台中的 Main 方法,它不能是异步的(在 C# 7.1 之前)。

如果您忽略此最佳实践,您将在 GUI 和 ASP.NET 应用程序中遇到死锁。发生死锁是因为这些应用程序在只允许一个线程并且不会将其交给异步线程的上下文中运行。这意味着 GUI 同步等待返回,而异步方法等待上下文:死锁。

此行为不会在控制台应用程序中发生,因为它在具有线程池的上下文中运行。 async 方法将在另一个将被调度的线程上返回。这就是为什么一个测试控制台应用程序可以工作,但相同的调用会在其他应用程序中死锁......

【讨论】:

“除了控制台中的 Main 方法,它不能是异步的。” 从 C# 7.1 开始,Main 现在可以是异步方法 link【参考方案3】:

您的代码并没有像您认为的那样做。异步方法在方法开始等待异步结果后立即返回。使用跟踪来调查代码的实际行为是很有见地的。

以下代码执行以下操作:

创建 4 个任务 每个任务都会异步增加一个数字并返回增加后的数字 当异步结果到达时,它会被跟踪。

 

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;

当你观察痕迹时

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   

您会注意到 Run 方法在线程 2820 上完成,而只有一个子线程完成 (2756)。如果您在 await 方法周围放置一个 try/catch,您可以以通常的方式“捕获”异常,尽管您的代码在计算任务完成并执行您的 contiuation 时在另一个线程上执行。

计算方法会自动跟踪抛出的异常,因为我确实使用了 ApiChange 工具中的 ApiChange.Api.dll。 Tracing and Reflector 有助于理解发生了什么。要摆脱线程,您可以创建自己的 GetAwaiter BeginAwait 和 EndAwait 版本,而不是包装任务,而是例如在您自己的扩展方法中进行延迟和跟踪。然后您将更好地了解编译器和 TPL 的作用。

现在您看到没有办法尝试/捕获您的异常,因为没有堆栈帧可供任何异常传播。在您启动异步操作后,您的代码可能会做一些完全不同的事情。它可能会调用 Thread.Sleep 甚至终止。只要剩下一个前台线程,您的应用程序就会愉快地继续执行异步任务。


您可以在异步操作完成后在异步方法中处理异常并回调 UI 线程。推荐的方法是使用TaskScheduler.FromSynchronizationContext。这只有在你有一个 UI 线程并且它不是很忙于其他事情时才有效。

【讨论】:

【参考方案4】:

同样重要的是要注意,如果您在异步方法上有 void 返回类型,您将丢失异常的时间堆栈跟踪。我建议按如下方式返回任务。将使调试变得更加容易。

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

【讨论】:

这将导致并非所有路径都返回值的问题,因为如果有异常则不会返回值,而在 try 中有。如果您没有 return 语句,则此代码仍然有效,因为 Task 是使用 async / await“隐式”返回的。【参考方案5】:

异常没有被捕获的原因是 Foo() 方法有一个 void 返回类型,所以当调用 await 时,它只是简单地返回。由于 DoFoo() 没有等待 Foo 的完成,因此无法使用异常处理程序。

如果您可以更改方法签名,这将打开一个更简单的解决方案 - 更改 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
    

【讨论】:

这真的可以偷偷摸摸,应该被编译器警告。【参考方案6】:

可以在异步函数中捕获异常。

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

【讨论】:

嘿,我知道,但我确实需要 DoFoo 中的信息,以便我可以在 UI 中显示这些信息。在这种情况下,UI 显示异常很重要,因为它不是最终用户工具,而是调试通信协议的工具 在这种情况下,回调很有意义。(好的旧异步委托) @Tim:在抛出的异常中包含您需要的任何信息? @EricJ。逻辑在 await 的一开始就结束了

以上是关于捕获异步 void 方法引发的异常的主要内容,如果未能解决你的问题,请参考以下文章

为啥我无法在具有 void 返回类型的异步函数中捕获异常?

csharp 无法捕获异步void异常

异步编程最佳实践

捕获调用异步方法C#的异常[重复]

来自异步方法的 Mvc .Net 捕获异常

令人惊讶的情况,异步方法中的异常处理不会捕获异常