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

上一篇文章裏咱們討論了某些async/await的用法中出現遺漏異常的狀況,而且談到該如何使用WhenAll輔助方法來避免這種狀況。WhenAll輔助方法將會彙總一系列的任務對象,一旦其中某個出錯,則會拋出「其中一個」異常。那麼到底是哪一個異常?若是咱們要處理全部的異常怎麼辦?咱們此次就來詳細討論await操做在異常分派時的相關行爲。html

 

 

await拋出異常時的行爲編程



要理解await的行爲,仍是從理解Task對象的異常表現開始。Task對象有一個Exception屬性,類型爲AggregateException,在執行成功的狀況下該屬性返回null,不然便包含了「全部」出錯的對象。既然是AggregateException,則意爲着可能包含多個子異常,這種狀況每每會在任務的父子關係中出現,具體狀況能夠參考MSDN中的相關說明。在許多狀況下一個Task內部只會出現一個異常,此時這個AggregateException的InnerExceptions屬性天然也就只一個元素。數組

 

Task對象自己還有一個Wait方法,它會阻塞當前執行代碼,直到任務完成。在出現異常的時候,它會將自身的AggregateException拋出:async

try
{
    t.Wait();
}
catch (AggregateException ex)
{
    ...
}

Wait方法是「真阻塞」,而await操做則是使用阻塞語義的代碼實現非阻塞的效果,這個區別必定要分清。與Wait方法不一樣的是,await操做符效果並不是是「拋出」Task對象上的Exception屬性,而只是拋出這個AggregateException對象上的「其中一個」元素。我在內部郵件列表中詢問這麼作的設計考慮,C#開發組的同窗回答道,這個決策在內部也經歷了激烈的爭論,最終的選擇這種方式而不是直接拋出Task對象上的AggregateException是爲了不編寫出冗餘的代碼,並讓代碼與傳統同步編程習慣更爲接近。spa


他們舉了一個簡單的示例,假如一個Task對象t可能拋出兩種異常,如今的錯誤捕獲方式爲:設計

try
{
    await t1;
}
catch (NotSupportedException ex)
{
    ...
}
catch (NotImplementedException ex)
{
    ...
}
catch (Exception ex)
{
    ...
}

假如await操做拋出的是AggregateException,那麼代碼就必須寫爲:code

try
{
    await t1;
}
catch (AggregateException ex)
{
    var innerEx = ex.InnerExceptions[0];

    if (innerEx is NotSupportedException)
    {
        ...
    }
    else if (innerEx is NotImplementedException)
    {
        ...
    }
    else
    {
        ...
    }
}

顯然前者更貼近傳統的同步編程習慣。可是問題在於,若是這個Task中包含了多個異常怎麼辦?以前的描述是拋出「其中一個」異常,對於開發者來講,「其中一個」這種模糊的說法天然沒法使人滿意,但事實的確如此。從內部郵件列表中的討論來看,C#開發團隊提到他們「故意」不提供文檔說明究竟會拋出哪一個異常,由於他們並不想作出這方面的約束,由於這部分行爲一旦寫入文檔,便成爲一個規定和限制,爲了類庫的兼容性從此也沒法對此作出修改。htm


他們也提到,若是單論目前的實現,await操做會從Task.Exception.InnerExceptions集合中挑出第一個異常,並對外「拋出」,這是System.Runtime.CompilerServices.TaskAwaiter類中定義的行爲。可是既然這並不是是「文檔化」的固定行爲,開發人員也儘可能不要依賴這點。對象

 

 

WhenAll的異常彙總方式blog



其實這個話題跟async/await的行爲沒有任何聯繫,WhenAll返回的是普通的Task對象,TaskAwaiter也絲絕不關心當前等待的Task對象是否來自於WhenAll,不過既然WhenAll是最經常使用的輔助方法之一,也順便將其講清楚吧。


WhenAll獲得Task對象,其結果是用數組存放的全部子Task的結果,而在出現異常時,其Exception屬性返回的AggregateException集合會包含全部子Task中拋出的異常。請注意,每一個子Task中拋出的異常將會存放在它自身的AggregateException集合中,WhenAll返回的Task對象將會「按順序」收集各個AggregateException集合中的元素,而並不是收集每一個AggregateException對象。


咱們使用一個簡單的例子來理解這點:

Task all = null;
try
{
    await (all = Task.WhenAll(
        Task.WhenAll(
            ThrowAfter(3000, new Exception("Ex3")),
            ThrowAfter(1000, new Exception("Ex1"))),
        ThrowAfter(2000, new Exception("Ex2"))));
}
catch (Exception ex)
{
    ...
}

這段代碼使用了嵌套的WhenAll方法,總共會出現三個異常,按其拋出的時機排序,其順序爲Ex1,Ex2及Ex3。那麼請問:

  1. catch語句捕獲的異常是哪一個?
  2. all.Exception這個AggregateException集合中異常按順序是哪些?

結果以下:

  1. catch語句捕獲的異常是Ex3,由於它是all.Exception這個AggregateException集合中的第一個元素,但仍是請牢記這點,這只是當前TaskAwaiter所實現的行爲,而並不是是由文檔規定的結果。
  2. all.Exception這個AggregateException集合中異常有三個,按順序是Ex3,Ex1和Ex2。WhenAll獲得的Task對象,是根據輸入的Task對象順序來決定自身AggreagteException集合中異常對象的存放順序。這個順序跟異常的拋出時機沒有任何關係。

這裏咱們也順即可以得知,若是您不想捕獲AggregateException集合中的「其中一個」異常,而是想處理全部異常的話,也能夠寫這樣的代碼:

Task all = null;
try
{
    await (all = Task.WhenAll(
        ThrowAfter(1000, new Exception("Ex1")),
        ThrowAfter(2000, new Exception("Ex2"))));
}
catch
{
    foreach (var ex in all.Exception.InnerExceptions)
    {
        ...
    }
}

固然,這裏使用Task.WhenAll做爲示例,是由於這個Task對象能夠明確包含多個異常,但並不是只有Task.WhenAll返回的Task對象纔可能包含多個異常,例如Task對象在建立時指定了父子關係,也會讓父任務裏包含各個子任務裏出現的異常。

 

 

假如異常未被捕獲



最後再來看一個簡單的問題,咱們一直在關注一個async方法中「捕獲」異常的行爲,假如異常沒有成功捕獲,直接對外拋出的時候,對任務自己的有什麼影響呢?且看這個示例:

static async Task SomeTask()
{
    try
    {
        await Task.WhenAll(
            ThrowAfter(2000, new NotSupportedException("Ex1")),
            ThrowAfter(1000, new NotImplementedException("Ex2")));
    }
    catch (NotImplementedException) { }
}

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

    SomeTask().ContinueWith(t => PrintException(t.Exception));

    Console.ReadLine();
}

這段代碼的輸出結果是:

System.AggregateException: One or more errors occurred. ---> System.NotSupportedException: Ex1
   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 30
   --- End of inner exception stack trace ---
---> (Inner Exception #0) System.NotSupportedException: Ex1
   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 30<---

AggregateException的打印內容不那麼容易讀,咱們能夠關注它Inner Exception #0這樣的信息。從時間上說,Ex2先於Ex1拋出,而catch的目標是NotImplementedException。但從以前的描述咱們能夠知道,WhenAll返回的Task內部的異常集合,與各異常拋出的時機沒有關係,所以await操做符拋出的是Ex1,是NotSupportedException,而它不會被catch到,所以SomeTask返回的Task對象也會包含這個異常——也僅僅是拋出這個異常,而Ex2對於外部就不可見了。


若是您想在外部處理全部的異常,則能夠這樣:

Task all = null;
try
{
    await (all = Task.WhenAll(
        ThrowAfter(2000, new NotSupportedException("Ex1")),
        ThrowAfter(1000, new NotImplementedException("Ex2"))));
}
catch
{
    throw all.Exception;
}

此時打印的結果即是一個AggregateException包含着另外一個AggregateException,其中包含了Ex1和Ex2。爲了「解開」這種嵌套關係,AggregateException也提供了一個Flatten方法,能夠將這種嵌套徹底「鋪平」,例如:

SomeTask().ContinueWith(t => PrintException(t.Exception.Flatten()));

此時打印的結果便直接是一個AggregateException包含着Ex1與Ex2了。

 

 

相關文章



關於C#中async/await中的錯誤處理(上)
關於C#中async/await中的錯誤處理(下)

 

 

原文連接

相關文章
相關標籤/搜索