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


281

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

因此,基本上,我希望异步代码中的异常冒充到我的调用代码中,即使有可能的话。



22
万一将来有人偶然发现它,Async / Await最佳实践...一文在“图2无法捕获到异步无效方法的异常”中对此做了很好的解释。“ 当从异步Task或异步Task <T>方法抛出异常时,该异常被捕获并放置在Task对象上。使用异步void方法,则没有Task对象,异步void方法抛出任何异常将直接在异步void方法启动时处于活动状态的SynchronizationContext上引发。
Moose先生

您可以使用此方法
Tselofan

Answers:


261

阅读起来有点怪异,但是可以,异常会冒泡到调用代码中-但仅当您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. */
    }
} 

异步void方法具有不同的错误处理语义。从异步Task或异步Task方法抛出异常时,将捕获该异常并将其放置在Task对象上。使用异步void方法时,没有Task对象,因此从异步void方法抛出的任何异常都将直接在启动异步void方法时处于活动状态的SynchronizationContext上引发。- https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

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

这个说明http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions非常好-它讨论了编译器为实现此魔术而采取的步骤。


3
实际上,我的意思是阅读很简单-但我知道实际发生的事情确实很复杂-所以我的大脑告诉我不要相信自己的眼睛……
Stuart

8
我认为Foo()方法应标记为Task而不是void。
Sornii 2014年

4
我很确定这会产生AggregateException。这样,此答案中出现的catch块将不会捕获异常。
xanadont 2014年

2
“但仅当您等待或对Foo进行调用的Wait()时” await,当Foo返回void时,如何调用Foo?async void Foo()Type void is not awaitable
rism

3
无法等待void方法,可以吗?
Hitesh P

74

未捕获到异常的原因是因为Foo()方法的返回类型为void,因此在调用await时,它只是返回。由于DoFoo()不等待Foo完成,因此无法使用异常处理程序。

如果您可以更改方法签名Foo(),则这将提供一个更简单的解决方案-alter ,使其返回type Task,然后DoFoo()可以can 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
    }
}

19
这确实可以偷偷摸摸,应该由编译器警告。
GGleGrand

19

您的代码没有执行您可能认为的操作。异步方法在开始等待异步结果之后立即返回。使用跟踪来调查代码的实际行为是很有见地的。

下面的代码执行以下操作:

  • 创建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上完成。如果将try / catch放在await方法周围,则可以以通常的方式“捕获”异常,尽管当计算任务完成并且执行了连续操作时,代码是在另一个线程上执行的。

因为我确实使用了ApiChange工具中的ApiChange.Api.dll,所以该计算方法会自动跟踪引发的异常。跟踪和反射器可以帮助您了解正在发生的事情。要摆脱线程,您可以创建自己的GetAwaiter BeginAwait和EndAwait版本,而不是包装一个任务,而是包装一个Lazy,并在您自己的扩展方法中进行跟踪。然后,您将更好地了解编译器和TPL的功能。

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


异步操作完成后,您可以在async方法内处理异常,并回调到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();
}

2
嘿,我知道,但是我确实需要DoFoo中的信息,以便可以在UI中显示该信息。在这种情况下,对于UI而言,显示异常非常重要,因为它不是最终用户工具,而是调试通信协议的工具
TimothyP 2011年

在这种情况下,回调很有意义。(好的异步代表)
Sanjeevakumar Hiremath 2011年

@Tim:在引发的异常中包括所需的任何信息?
Eric J.

5

同样重要的是要注意,如果异步方法的返回类型为空,则将丢失按时间顺序排列的异常的堆栈跟踪。我建议按以下方式返回Task。使调试变得更加容易。

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

这将导致并非所有路径都返回值的问题,因为如果有异常,则不会返回任何值,而在尝试时会返回任何值。如果没有return语句,则此代码有效,因为使用Task会“隐式”返回async / await
Matias Grioni '19

2

该博客巧妙地解释了您的问题异步最佳实践

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

最佳实践是将返回类型更改为Task。另外,尝试通过所有方式编写异步代码,进行每个异步方法调用并从异步方法中调用。控制台中的Main方法除外,该方法不能异步(在C#7.1之前)。

如果您忽略此最佳做法,则会遇到GUI和ASP.NET应用程序的死锁。之所以会出现死锁,是因为这些应用程序在仅允许一个线程且不会将其放弃给异步线程的上下文中运行。这意味着GUI同步等待返回,而async方法等待上下文:死锁。

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


1
“控制台中的Main方法除外,该方法不能异步。” 从C#7.1开始,Main现在可以成为异步方法链接
Adam
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.