《C#併發編程經典實例》學習筆記—2.4 等待一組任務完成

問題

執行幾個任務,等待它們全部完成。

使用場景

  • 幾個獨立任務需要同時進行
  • UI界面加載多個模塊,併發請求

解決方案

Task.WhenAll 傳入若干任務,當所有任務完成時,返回一個完成的任務。

重載方法

  • Task WhenAll(IEnumerable<Task>)
  • Task WhenAll(params Task[])
  • Task<TResult[]> WhenAll<TResult>(IEnumerable<Task<TResult>>)
  • Task<TResult[]> WhenAll<TResult>(params Task<TResult>[])
    舉例:
            var task1 = Task.Delay(TimeSpan.FromSeconds(1));
            var task2 = Task.Delay(TimeSpan.FromSeconds(2));
            var task3 = Task.Delay(TimeSpan.FromSeconds(3));

            await Task.WhenAll(task1, task2, task3);

當任務返回結果類型相同,所有任務完成返回的是,存着每個任務執行結果的數組。

            var task1 = Task.FromResult(1);
            var task2 = Task.FromResult(2);
            var task3 = Task.FromResult(3);

            int[] array = await Task.WhenAll(task1, task2, task3); //{1,2,3}

返回的數組中結果的順序,並非可控,如上述例子中,只是結果爲包含了1,2,3的數組,順序是不定的。

書中不建議使用以 IEnumerable 類型作爲參數的重載。文中沒有介紹作者不建議的原因。我找到作者個人博客的一篇文中中提到如下文字(文章地址:https://blog.stephencleary.com/2015/01/a-tour-of-task-part-7-continuations.html

The IEnumerable<> overloads allow you to pass in a sequence of tasks, such as a LINQ expression. The sequence is immediately reified (i.e., copied to an array). For example, this allows you to pass the results of a Select expression directly to WhenAll. Personally, I usually prefer to explicitly reify the sequence by calling ToArray() so that it’s obvious that’s what’s happening, but some folks like the ability to pass the sequence directly in.

該段文字解釋了作者更喜歡使用LINQ結合ToArray的方式使用異步,因爲作者認爲代碼會更清晰。書中有例子,如下所示:

        static async Task<string> DownloadAllAsync(IEnumerable<string> urls)
        {
            var httpClient = new HttpClient();
            // 定義每一個 url 的使用方法。
            var downloads = urls.Select(url => httpClient.GetStringAsync(url));
            // 注意,到這裏,序列還沒有求值,所以所有任務都還沒真正啓動。
            // 下面,所有的 URL 下載同步開始。
            Task<string>[] downloadTasks = downloads.ToArray();
            // 到這裏,所有的任務已經開始執行了。
            // 用異步方式等待所有下載完成。
            string[] htmlPages = await Task.WhenAll(downloadTasks);
            return string.Concat(htmlPages);
        }

如果報錯記得添加如下引用

using System.Linq;
using System.Net.Http;

返回的Task的狀態

            var task1 = ......;
            var task2 = ......;
            var task3 = ......;

            Task allTasks = Task.WhenAll(task1, task2, task3);

以上述僞代碼爲例說明allTasks的狀態

  • 當task1出現異常,異常會拋給allTasks,allTasks的狀態同task1狀態,也是Faulted。
  • 當task1被取消,allTasks的狀態是Canceled
  • 當task1, task2, task3,不出現異常,也未被取消,allTasks的狀態是RanToCompletion

Task.WhenAll的異常處理

上面提到了異常處理,當一個task異常,該異常會被allTasks接收,當多個task異常,這些異常也都會被allTasks接收。但是task1拋異常,task2也出異常,但是try catch 處理await Task.WhenAll(task1, task2);只能抓取其中某一個異常。如何獲取所有異常呢?書中列舉了兩種處理方法,代碼如下
拋出異常的方法

        static async Task ThrowNotImplementedExceptionAsync()
        {
            throw new NotImplementedException();
        }

        static async Task ThrowInvalidOperationExceptionAsync()
        {
            throw new InvalidOperationException();
        }

第一種處理方式,只能獲取其中一個異常

        static async Task ObserveOneExceptionAsync()
        {
            var task1 = ThrowNotImplementedExceptionAsync();
            var task2 = ThrowInvalidOperationExceptionAsync();
            try
            {
                await Task.WhenAll(task1, task2);
            }
            catch (Exception ex)
            {
                // ex 要麼是 NotImplementedException,要麼是 InvalidOperationException
                //...
            }
        }

第二種處理方式,可以獲取所有異常

        static async Task ObserveAllExceptionsAsync()
        {
            var task1 = ThrowNotImplementedExceptionAsync();
            var task2 = ThrowInvalidOperationExceptionAsync();
            Task allTasks = Task.WhenAll(task1, task2);
            try
            {
                await allTasks;
            }
            catch
            {
                AggregateException allExceptions = allTasks.Exception;
                //...
            }
        }

兩種方式的區別是,await調用Task.WhenAll返回的Task對象,即例子中的allTasks,代碼await allTasks;

作者在書中將對Task.WhenAll的異常處理放在了討論中,並說明了自己的處理方式

使用 Task.WhenAll 時,我一般不會檢查所有的異常。通常情況下,只處理第一個錯誤就足夠了,沒必要處理全部錯誤。

顯然作者更中意第一種方式。那麼你呢?

參考文章:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.task.whenall?view=netcore-2.2

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章