《C#併發編程經典實例》學習筆記—2.5 等待任意一個任務完成 Task.WhenAny

問題

執行若干個任務,只需要對其中任意一個的完成進行響應。這主要用於:對一個操作進行多種獨立的嘗試,只要一個嘗試完成,任務就算完成。例如,同時向多個 Web 服務詢問股票價格,但是隻關心第一個響應的。

文中舉的是向多個Web服務詢問股票價格的例子。
我曾在過往的工作中遇到另一個不太相似的例子。一個問答項目,在問題詳情頁面,重要的是問題展示和回答展示。在該頁面有相關房型推薦和類似問題推薦等等多個模塊展示。也就是說在請求問題數據之外還需要請求多個接口,按理說這個時候最適合的是使用Task.WhenAll,但是當時情形下因爲服務器性能受限導致頁面加載過慢影響用戶訪問,所以其時最快需要解決的是頁面加載過慢的問題,所以這時使用Task.WhenAny或許也算得上是一個應急折中的方案,當然這裏不提緩存等其他優化方案。

首先查看官方文檔,瞭解所有重載和返回值:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.task.whenany?view=netcore-2.2

Task.WhenAny與Task.WhenAll比較:

  • 相同點:參數都是一批任務
  • 不同點:Task.WhenAny返回的是完成的任務。

關於返回值的描述有點不太好理解。結合代碼很容易就能明白。

// 返回第一個響應的 URL 的數據長度。
private static async Task<int> FirstRespondingUrlAsync(string urlA, string urlB)
{
         var httpClient = new HttpClient();
         // 併發地開始兩個下載任務。
         Task<byte[]> downloadTaskA = httpClient.GetByteArrayAsync(urlA);
         Task<byte[]> downloadTaskB = httpClient.GetByteArrayAsync(urlB);
         // 等待任意一個任務完成。
         Task<byte[]> completedTask = await Task.WhenAny(downloadTaskA, downloadTaskB);
         // 返回從 URL 得到的數據的長度。
         byte[] data = await completedTask;
         return data.Length;
}

注意Task<byte[]> completedTask = await Task.WhenAny(downloadTaskA, downloadTaskB);,使用await獲取返回結果仍然是一個Task任務,如果Task.WhenAny返回的Task有異常,這行代碼並不會拋出異常,而是在byte[] data = await completedTask; 這裏拋出異常。
在第一個任務完成之後,如果其他任務沒有被取消,也不曾await,那麼這些任務將被遺棄,被遺棄的任務並不是代表任務停止,而是任務繼續執行直到完成,當然這些被遺棄的任務的結果或異常都會被忽略。

文中提到了另外兩個對WhenAny的使用方法。我試着寫了demo。

使用Task.WhenAny實現超時功能

書中給出的思路是其中一個任務是Delay的,這樣返回的第一個任務如果是該Delay任務。文中不推薦該方式,因爲沒有取消功能,即其他超時的任務不能被取消,無疑對計算機資源是一種浪費,對性能也會造成影響。

        // 返回第一個響應的 URL 的數據長度。
        private static async Task<int> FirstRespondingUrlAsync(string urlA, string urlB)
        {
            var httpClient = new HttpClient();
            // 併發地開始兩個下載任務。
            Task<byte[]> downloadTaskA = httpClient.GetByteArrayAsync(urlA);
            Task<byte[]> downloadTaskB = httpClient.GetByteArrayAsync(urlB);
            Task<byte[]> delayTask = GetDelayTask();
            // 等待任意一個任務完成。
            Task<byte[]> completedTask = await Task.WhenAny(downloadTaskA, downloadTaskB, delayTask);
            // 返回從 URL 得到的數據的長度。
            byte[] data = await completedTask;
            if (data.Length == 1 && data[0] == byte.MaxValue)
            {
                Console.WriteLine("超時提醒");
            }
            return data.Length;
        }

       
        // 獲取超時任務
        private static Task<byte[]> GetDelayTask()
        {
            return new Task<byte[]>(() =>
            {
                Task.Delay(1000);
                return new[] { byte.MaxValue };
            });
        }

使用Task.WhenAny處理已完成的任務

書中給出的思路是,列表存放Task,完成一個任務就移除一個已完成的Task。文中不推薦此方法,因爲執行時間是 O(N^2),2.6小節有 O(N) 的算法。

        // 處理已完成的任務
        private static async Task ProcessTasksAsync(string urlA, string urlB)
        {
            var httpClient = new HttpClient();
            // 併發地開始兩個下載任務。
            Task<byte[]> downloadTaskA = httpClient.GetByteArrayAsync(urlA);
            Task<byte[]> downloadTaskB = httpClient.GetByteArrayAsync(urlB);
            var tasks = new List<Task<byte[]>> { downloadTaskA, downloadTaskB };
            while (true)
            {
                // 等待任意一個任務完成。
                Task<byte[]> completedTask = await Task.WhenAny(tasks);
                //移除已完成的任務
                tasks.Remove(completedTask);
                if (!tasks.Any())
                {
                    break;
                }
            }
        }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章