程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 關於C#中async/await中的異常處理(上)

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

編輯:C#入門知識

在同步編程中,一旦出現錯誤就會拋出異常,我們可以使用try…catch來捕捉異常,而未被捕獲的異常則會不斷向上傳遞,形成一個簡單而統一的錯誤處理機制。不過對於異步編程來說,異常處理一直是件麻煩的事情,這也是C#中async/await或是Jscex等異步編程模型的優勢之一。但是,同步的錯誤處理機制,並不能完全避免異步形式的錯誤處理方式,這需要一定實踐規范來保證,至少我們需要了解async/await到底是如何捕獲和分發異常的。在開發Jscex的過程中,我也在C#內部郵件郵件列表中了解了很多關於TPL和C#異步特性的問題,錯誤處理也是其中之一。在此記錄一下吧。
使用try…catch捕獲異常
首先我們來看
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到哪裡去了?
未捕獲的異常
C#的async/await功能基於TPL的Task對象,每個await操作符都是“等待”一個Task完成。在之前(或者說如今)的TPL中,Task對象的析構函數會查看它的Exception對象有沒有被“訪問”過,如果沒有,且Task對象出現了異常,則會拋出這個異常,最終導致的結果往往便是進程退出。因此,我們必須小心翼翼地處理每一個Task對象的錯誤,不得遺漏。在.NET 4.5中這個行為被改變了,對於任何沒有被檢查過的異常,便會觸發TaskSchedular.UnobservedTaskException事件——如果您不監聽這個事件,未捕獲的異常也就這麼無影無蹤了。
為此,我們對Main方法進行一個簡單的改造。
static void Main(string[] args)
{
    TaskScheduler.UnobservedTaskException += (_, ev) => PrintException(ev.Exception);

    _watch.Start();

    MissHandling();

    while (true)
    {
        Thread.Sleep(1000);
        GC.Collect();
    }
}
改造有兩點,一是響應TaskScheduler.UnobservedTaskException,這自然不必多說。還有一點便是不斷地觸發垃圾回收,以便Finalizer線程調用析構函數。如今這段代碼除了打印出之前的信息之外,還會輸出以下內容:
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對象失敗了:則拋出“其中一個”異常。
全部成功的情況自不必說,那麼在失敗的情況下,什麼叫做拋出“其中一個”異常?如果我們要處理所有拋出的異常該怎麼辦?下次我們繼續討論這方面的問題。

 
摘自  老趙點滴-追求編程之美

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved