前言
之前自己對於 Task.WaitAll 與 WhenAll 有些一知半解的地方,因此進行研究後並撰寫了 [.NET] Task 等待多個任務 - Task.WaitAll 與 Task.WhenAll 文章,最近在與公司同事 Code Review 再度討論起兩者的使用方式,主管也不吝的與團隊成員分享對於 WaitAll 與 WhenAll 主要的看法與使用上的差異,這篇筆記簡單記錄當時討論的內容與結論,內容若有問題歡迎提出來一起討論。
首先,在之前文章[.NET] Task 等待多個任務 - Task.WaitAll 與 Task.WhenAll 中思考的出發點是以效率出發,而建議使用 Task.WhenAll 取代 Task.WaitAll 方法,但可能在使用上還是有其他需要考慮的部分,其實效率並不是兩個方法的主要差異,差異性有以下兩點
例外處理 Exception
我們接下來可以透過簡單的 Code 來說明例外處理的部分,下列代碼定義了兩個 Function 分別是 throw IndexOutOfRangeException 與 NullReferenceException,在 main 的方法使用 WaitAll 並使用 try catch 將觀察例外處理 exception 部分為何
- Task.WaitAll() 處理所有返回 Task 的 Exception
- Task.WhenAll() 只能處理第一個返回 Task 的 Exception
開啟 Rider在 catch 下中斷點,可以從 Console 可以看到有抓到兩個 exception,拋出的例外 excption 都有抓到看起來合情合理 接著將 WhenAll 來替換定原有的 WaitAll 方法,再重新跑一次觀察例外處理 exception 是否相同
- class Program
- {
- static async Task Main(string[] args)
- {
- try
- {
- Task.WaitAll(ThrowIndexOfRangeException(), ThrowNullReferenceException());
- }
- catch (Exception ex)
- {
- Console.WriteLine(ex);
- }
- }
- static private Task ThrowIndexOfRangeException()
- {
- return Task.Run(() => { throw new IndexOutOfRangeException();});
- }
- static private Task ThrowNullReferenceException()
- {
- return Task.Run(() => { throw new NullReferenceException(); });
- }
- }
執行結果如下 可以發現 exception 僅僅抓到第一個拋出的例外也就是 indexOutOfRangeException,並不會抓到拋出來的第二個例外,也就是說 catch 抓到的 exception 僅僅是第一個 exception 其餘的都被河蟹掉了(希望的可能是 catch 到所有的例外),有開發者在微軟的 Github 反映 Task.WhenAll - Inner exceptions are lost #31494 此問題,內部有針對此問題做深入討論,原先設計方向是使用 WhenAll 時會發生例外時會將錯誤包在 AggregateException 中,後來因為下列原因修改這設計(怕誤解意思直接使用原文)
- static async Task Main(string[] args)
- {
- var tasks = new [] { ThrowIndexOfRangeException(), ThrowNullReferenceException() };
- try
- {
- await Task.WhenAll(tasks);
- }
- catch (Exception ex)
- {
- Console.WriteLine(ex);
- }
- }
了解設計初衷之後,如果還是希望在 WhenAll 取得所有的例外可以參考討論串中 noseratio 的回答撰寫 Task 擴充方法蒐集 exception ,代碼如下
- a) the vast majority of such cases had fairly homogenous exceptions, such that propagating all in an aggregate wasn't that important
- b) propagating the aggregate then broke expectations around catches for the specific exception types, and
- c) for cases where someone did want the aggregate, they could do so explicitly with the two lines like I wrote.
加入擴充方法後,下一步就是將既有的 WhenAll 代碼加上 WithAggregatedExceptions 擴充方法
- public static Task WithAggregatedExceptions(this Task @this)
- {
- return @this.ContinueWith(ante =>
- ante.IsFaulted &&
- (ante.Exception.InnerExceptions.Count > 1 ||
- ante.Exception.InnerException is AggregateException) ?
- Task.FromException(ante.Exception.Flatten()) :
- ante,
- TaskContinuationOptions.ExecuteSynchronously).Unwrap();
- }
再重新看一下 Catch 到的錯誤種類,即可發現被河蟹掉的都找的到了 (大師兄回來了 主線程阻塞
- static async Task Main(string[] args)
- {
- var tasks = new [] { ThrowIndexOfRangeException(), ThrowNullReferenceException() };
- try
- {
- await Task.WhenAll(tasks).WithAggregatedExceptions();
- }
- catch (Exception ex)
- {
- Console.WriteLine(ex);
- }
- }
另一個要關注的點是使用後是否會造成阻塞的狀況,由於主線程雍塞的議題已經不是一兩天的事,這裡就簡單整理大神所提到的重點
- 是否會造成主線程雍塞,影響用戶使用
ASP.NET async 基本心法
閱讀筆記 - 使用 .NET Async/Await 的常見錯誤
.NET 程式鎖死與 SynchronizationContext
或是參考由強者前同事所撰寫的電子書 : .NET 本事-非同步程式設計 :)
感想
魔鬼藏在細節裡。以上就針對 Task.WaitAll 與 Task.WhenAll 做更進一步的說明,以及在討論時所提到的兩個主要的差異內容,這些細節如果一沒有注意到勢必會造成很大的影響,在開發使用也請多加留意或是查相關資料。
參考
C# Thread: What is the difference between Task.WaitAll & Task.WhenAll
Why doesn't await on Task.WhenAll throw an AggregateException?
Task.WhenAll Method
Task.WhenAll - Inner exceptions are lost
0 意見:
張貼留言