關於C#中async/await中的異常處理(上)-(轉載)

在同步編程中,一旦出現錯誤就會拋出異常,咱們可使用try…catch來捕捉異常,而未被捕獲的異常則會不斷向上傳遞,造成一個簡單而統一的錯誤處理機制。不過對於異步編程來講,異常處理一直是件麻煩的事情,這也是C#中async/await或是Jscex等異步編程模型的優點之一。可是,同步的錯誤處理機制,並不能徹底避免異步形式的錯誤處理方式,這須要必定實踐規範來保證,至少咱們須要瞭解async/await究竟是如何捕獲和分發異常的。在開發Jscex的過程當中,我也在C#內部郵件郵件列表中瞭解了不少關於TPL和C#異步特性的問題,錯誤處理也是其中之一。在此記錄一下吧。html

 

 

使用try…catch捕獲異常git



首先咱們來看下這段代碼:github

static async Task ThrowAfter(int timeout, Exception ex)
{
    await Task.Delay(timeout);
    throw ex;
}

static void PrintException(Exception ex)
{
    Console.WriteLine("Time: {0}\n{1}\n============", _watch.Elapsed, ex);
}

static Stopwatch _watch = new Stopwatch();

static async Task MissHandling()
{
    var t1 = ThrowAfter(1000, new NotSupportedException("Error 1"));
    var t2 = ThrowAfter(2000, new NotImplementedException("Error 2"));

    try
    {
        await t1;
    }
    catch (NotSupportedException ex)
    {
        PrintException(ex);
    }
}

static void Main(string[] args)
{
    _watch.Start();

    MissHandling();

    Console.ReadLine();
}

這段代碼的輸出以下:編程

Time: 00:00:01.2058970
System.NotSupportedException: Error 1
   at AsyncErrorHandling.Program.d__0.MoveNext() in ...\Program.cs:line 16
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at AsyncErrorHandling.Program.d__3.MoveNext() in ...\Program.cs:line 33
============

在MissingHandling方法中,咱們首先使用ThrowAfter方法開啓兩個任務,它們會分別在一秒及兩秒後拋出兩個不一樣的異常。可是在接下來的try中,咱們只對t1進行await操做。很容易理解,t1拋出的NotSupportedException將被catch捕獲,耗時大約爲1秒左右——固然,從上面的數據能夠看出,其實t1在被「捕獲」時已經耗費了1.2時間,偏差較大。這是由於程序剛啓動,TPL內部正處於「熱身」狀態,在調度上會有較大開銷。這裏反卻是另外一個問題倒更值得關注:t2在兩秒後拋出的NotImplementedException到哪裏去了?數組

 

 

未捕獲的異常app



C#的async/await功能基於TPL的Task對象,每一個await操做符都是「等待」一個Task完成。在以前(或者說現在)的TPL中,Task對象的析構函數會查看它的Exception對象有沒有被「訪問」過,若是沒有,且Task對象出現了異常,則會拋出這個異常,最終致使的結果每每即是進程退出。所以,咱們必須當心翼翼地處理每個Task對象的錯誤,不得遺漏。在.NET 4.5中這個行爲被改變了,對於任何沒有被檢查過的異常,便會觸發TaskSchedular.UnobservedTaskException事件——若是您不監聽這個事件,未捕獲的異常也就這麼無影無蹤了。異步


爲此,咱們對Main方法進行一個簡單的改造。async

static void Main(string[] args)
{
    TaskScheduler.UnobservedTaskException += (_, ev) => PrintException(ev.Exception);

    _watch.Start();

    MissHandling();

    while (true)
    {
        Thread.Sleep(1000);
        GC.Collect();
    }
}

改造有兩點,一是響應TaskScheduler.UnobservedTaskException,這天然沒必要多說。還有一點即是不斷地觸發垃圾回收,以便Finalizer線程調用析構函數。現在這段代碼除了打印出以前的信息以外,還會輸出如下內容:ide

Time: 00:00:03.0984560
System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.NotImplementedException: Error 2
   at AsyncErrorHandling.Program.d__0.MoveNext() in ...\Program.cs:line 16
   --- End of inner exception stack trace ---
---> (Inner Exception #0) System.NotImplementedException: Error 2
   at AsyncErrorHandling.Program.d__0.MoveNext() in ...\Program.cs:line 16<---
============

從上面的信息中能夠看出,UnobservedTaskException事件並不是在「拋出」異常後便當即觸發,而是在某次垃圾收集過程,從Finalizer線程裏觸發並執行。從中也不可貴出這樣的結論:即是該事件的響應方法不能過於耗時,更加不能阻塞,不然便會對程序性能形成災難性的影響。異步編程


那麼假如咱們要同時處理t1和t2中拋出的異常該怎麼作呢?此時即是Task.WhenAll方法上場的時候了:

static async Task BothHandled()
{
    var t1 = ThrowAfter(1000, new NotSupportedException("Error 1"));
    var t2 = ThrowAfter(2000, new NotImplementedException("Error 2"));
    
    try
    {
        await Task.WhenAll(t1, t2);
    }
    catch (NotSupportedException ex)
    {
        PrintException(ex);
    }
}

若是您執行這段代碼,會發現其輸出與第一段代碼相同,但其實不一樣的是,第一段代碼中t2的異常被「遺漏」了,而目前這段代碼t1和t2的異常都被捕獲了,只不過await語句僅僅「拋出」了「其中一個」異常而已。


WhenAll是一個輔助方法,它的輸入是n個Task對象,輸出則是個返回它們的結果數組的Task對象。新的Task對象會在全部輸入所有「結束」後才完成。在這裏「結束」的意思包括成功和失敗(取消也是失敗的一種,即拋出了OperationCanceledException)。換句話說,假如這n個輸入中的某個Task對象很快便失敗了,也必須等待其餘全部輸入對象成功或是失敗以後,新的Task對象纔算完成。而新的Task對象完成後又可能會有兩種表現:

  • 全部輸入Task對象都成功了:則返回它們的結果數組。
  • 至少一個輸入Task對象失敗了:則拋出「其中一個」異常。

所有成功的狀況自沒必要說,那麼在失敗的狀況下,什麼叫作拋出「其中一個」異常?若是咱們要處理全部拋出的異常該怎麼辦?下次咱們繼續討論這方面的問題。

 

 

相關文章



關於C#中async/await中的異常處理(上)
關於C#中async/await中的異常處理(下)

 

 

原文連接

相關文章
相關標籤/搜索