捕获异步 void 方法抛出的异常

357

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

基本上,我希望从异步代码中抛出的异常能够传递到我的调用代码中,如果可能的话。


1
这个对你有帮助吗?http://social.msdn.microsoft.com/Forums/en/async/thread/ea741e14-0ba1-4001-ae7b-67ddb56911fd - svrist
35
如果有人在未来偶然发现了这个问题,Async/Await最佳实践...文章 在“图2 异步Void方法中的异常无法使用Catch捕获”一节中对此进行了很好的解释。 "当异步任务或异步任务T方法抛出异常时,该异常被捕获并放置在Task对象上。但是,对于异步void方法,没有Task对象,任何从异步void方法抛出的异常将直接在异步void方法启动时处于活动状态的同步上下文上引发。" - Mr Moose
你可以使用这种方法或者这个 - Tselofan
1
@MrMoose 什么是同步上下文(SynchronizationContext)?在其中引发异常意味着什么? - Marcos Pereira
1
@MarcosPereira,很抱歉我无法给你一个确切的答案,但它是System.Threading命名空间的一部分。请查看文档的备注部分相关链接以获取更多信息。 - Mr Moose
6个回答

339

读起来有点奇怪,但是是的,异常会向上传递到调用代码 - 但仅当你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在 异步/等待-异步编程中的最佳实践 中所写:

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

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

这篇解释http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions相当好 - 它讨论了编译器执行此操作的步骤。


3
实际上,我指的是它很容易阅读——尽管我知道实际情况非常复杂,所以我的大脑告诉我不要相信我的眼睛... - Stuart
8
我认为Foo()方法应该标记为Task而不是void。 - Sornii
6
我很确定这会产生一个AggregateException。因此,按照这个答案中的写法,catch块将无法捕获该异常。 - xanadont
3
但是只有在等待或使用Wait()调用Foo时,您才能“await”对Foo的调用。当Foo返回void时,您如何“await”对其的调用?async void Foo()Type void is not awaitable - rism
5
不能等待 void 方法,对吗? - Hitesh P
显示剩余9条评论

83

异常未被捕获的原因是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
    }
}

28
这可能会悄悄地出现在你身上,编译器应该对此进行警告。 - GGleGrand

20
你的代码可能不会像你想的那样运行。异步方法在开始等待异步结果后立即返回。使用跟踪功能来调查代码实际运行情况是很有见地的。
以下代码执行以下操作:
- 创建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   

您会注意到,当只有一个子线程完成(2756)时,Run方法在2820线程上完成。如果在await方法周围放置try/catch,您可以以通常的方式“捕获”异常,尽管在计算任务完成并执行您的contination时,您的代码在另一个线程上执行。
由于我使用了ApiChange工具中的ApiChange.Api.dll,因此计算方法会自动跟踪抛出的异常。跟踪和反编译有助于理解发生了什么。为了摆脱线程,您可以创建自己的GetAwaiter BeginAwait和EndAwait版本,并包装不是任务而是例如Lazy,在自己的扩展方法内部进行跟踪。然后,您将更好地了解编译器和TPL的操作。
现在,您会发现没有办法将异常返回到try/catch中,因为没有堆栈帧可用于传播任何异常。在启动异步操作后,您的代码可能正在执行完全不同的操作。它可能调用Thread.Sleep甚至终止。只要还有一个前台线程,您的应用程序就会愉快地继续执行异步任务。

在异步操作完成并回调到UI线程后,您可以在异步方法内处理异常。建议的做法是使用TaskScheduler.FromSynchronizationContext。这仅适用于具有UI线程且未忙于其他事情的情况。


5
异常可以在异步函数中捕获。
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();
}

3
嗨,我知道,但我真的需要在DoFoo中获取那些信息,这样我才能在用户界面中显示它们。在这种情况下,将异常信息显示在UI上很重要,因为它不是面向最终用户的工具,而是用于调试通信协议的工具。 - TimothyP
在这种情况下,回调函数非常有意义。(好老的异步委托) - Sanjeevakumar Hiremath
@Tim:在抛出异常时包含你需要的任何信息? - Eric J.
@EricJ。逻辑在await的开始处结束。 - Deepak

5

需要注意的是,如果您在异步方法中有一个void返回类型,您将丢失异常的时间顺序堆栈。建议按照以下方式返回任务(Task),这样可以大大简化调试过程。

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

1
这会导致一个问题,并非所有路径都返回值,因为如果出现异常,则不返回任何值,但在try中是有返回值的。如果您没有return语句,而使用async / await,则这段代码可以正常工作,因为Task是“隐式”返回的。 - Matias Grioni

5
这篇博客很好地解释了你的问题异步最佳实践
核心观点是,除非是异步事件处理程序,否则不应该将void作为异步方法的返回类型,这是一种不好的做法,因为它不允许捕获异常 ;-)
最佳实践是将返回类型更改为Task。此外,尽量在整个代码中都使用异步,使每个异步方法调用和被异步方法调用。除了控制台中的Main方法(在C# 7.1之前)。
如果忽略这个最佳实践,你将在GUI和ASP.NET应用程序中遇到死锁问题。死锁发生是因为这些应用程序运行在只允许一个线程的上下文中,并且不会将其释放给异步线程。这意味着GUI会同步等待返回,而异步方法则等待上下文:死锁。
这种行为在控制台应用程序中不会发生,因为它运行在具有线程池的上下文中。异步方法将在另一个线程上返回,并进行调度。这就是为什么测试控制台应用程序可以正常工作,但相同的调用会在其他应用程序中发生死锁的原因...

3
除了控制台中的 Main 方法,它不能是异步的。自从 C# 7.1 版本以来,Main 现在可以是一个异步方法。链接 - Adam

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接