簡體   English   中英

捕獲由 async void 方法拋出的異常

[英]Catch an exception thrown by an async void 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. */
    }
}

所以基本上,如果可能的話,我希望異步代碼中的異常冒泡到我的調用代碼中。

讀起來有點奇怪,但是是的,異常會冒泡到調用代碼——但前提是你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 - 異步編程的最佳實踐中所寫:

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() 方法具有 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
    }
}

您的代碼並不像您認為的那樣工作。 異步方法在方法開始等待異步結果后立即返回。 使用跟蹤來調查代碼的實際行為是很有見地的。

下面的代碼執行以下操作:

  • 創建 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 和 Reflector 對理解正在發生的事情有很大幫助。 要擺脫線程,您可以創建自己的 GetAwaiter BeginAwait 和 EndAwait 版本,而不是包裝任務,而是例如 Lazy 並在您自己的擴展方法中進行跟蹤。 然后您將更好地理解編譯器和 TPL 的作用。

現在您看到沒有辦法返回 try/catch 異常,因為沒有堆棧框架可供任何異常從中傳播。 在您啟動異步操作后,您的代碼可能會做一些完全不同的事情。 它可能會調用 Thread.Sleep 甚至終止。 只要還剩下一個前台線程,您的應用程序就會愉快地繼續執行異步任務。


在異步操作完成並回調到 UI 線程后,您可以在異步方法中處理異常。 推薦的方法是使用TaskScheduler.FromSynchronizationContext 只有當你有一個 UI 線程並且它不是很忙於其他事情時才有效。

同樣重要的是要注意,如果異步方法的返回類型為 void,您將丟失異常的時間順序堆棧跟蹤。 我建議按如下方式返回 Task。 將使調試變得更加容易。

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

該博客巧妙地解釋了您的問題Async Best Practices

它的要點是你不應該使用 void 作為異步方法的返回值,除非它是一個異步事件處理程序,這是不好的做法,因為它不允許捕獲異常;-)。

最佳做法是將返回類型更改為任務。 另外,嘗試一直編寫異步代碼,進行每個異步方法調用並從異步方法中調用。 除了控制台中的 Main 方法,它不能是異步的(在 C# 7.1 之前)。

如果忽略此最佳實踐,您將遇到 GUI 和 ASP.NET 應用程序的死鎖。 發生死鎖是因為這些應用程序運行在只允許一個線程並且不會將其交給異步線程的上下文中。 這意味着 GUI 同步等待返回,而異步方法等待上下文:死鎖。

這種行為不會發生在控制台應用程序中,因為它在具有線程池的上下文中運行。 async 方法將在另一個將被調度的線程上返回。 這就是為什么測試控制台應用程序可以工作,但相同的調用會在其他應用程序中死鎖......

可以在異步函數中捕獲異常。

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