在非同步工作完成時進行處理 (C#)
使用 Task.WhenAny,即可同時啟動多個工作,並在完成時逐一進行處理,而不是依啟動順序進行處理。
下列範例使用查詢來建立工作集合。 每項工作都會下載所指定網站的內容。 在 while 迴圈的的每個反覆運算中,等候的 WhenAny 呼叫會先傳回完成其下載的工作集合中的工作。 該工作會從集合中予以移除,並進行處理。 迴圈會重複進行,直到集合未包含其他工作為止。
必要條件
您可以使用下列其中一個選項來遵循此教學課程:
- 已安裝 .NET 桌面開發工作負載的 Visual Studio 2022。 您選取此工作負載時,會自動安裝 .NET SDK。
- .NET SDK 具有您選擇的程式碼編輯器,例如 Visual Studio Code。
建立範例應用程式
建立新的 .NET Core 主控台應用程式。 您可以使用 dotnet new console 命令或從 Visual Studio 建立一個。
在程式碼編輯器中開啟 Program.cs 檔案,並以下列程式碼取代現有的程式碼:
using System.Diagnostics;
namespace ProcessTasksAsTheyFinish;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
新增欄位
在 Program
類別定義中,新增下列兩個欄位:
static readonly HttpClient s_client = new HttpClient
{
MaxResponseContentBufferSize = 1_000_000
};
static readonly IEnumerable<string> s_urlList = new string[]
{
"https://learn.microsoft.com",
"https://learn.microsoft.com/aspnet/core",
"https://learn.microsoft.com/azure",
"https://learn.microsoft.com/azure/devops",
"https://learn.microsoft.com/dotnet",
"https://learn.microsoft.com/dynamics365",
"https://learn.microsoft.com/education",
"https://learn.microsoft.com/enterprise-mobility-security",
"https://learn.microsoft.com/gaming",
"https://learn.microsoft.com/graph",
"https://learn.microsoft.com/microsoft-365",
"https://learn.microsoft.com/office",
"https://learn.microsoft.com/powershell",
"https://learn.microsoft.com/sql",
"https://learn.microsoft.com/surface",
"https://learn.microsoft.com/system-center",
"https://learn.microsoft.com/visualstudio",
"https://learn.microsoft.com/windows",
"https://learn.microsoft.com/maui"
};
HttpClient
表示能夠傳送 HTTP 要求及接收 HTTP 回應。 s_urlList
會保存應用程式計劃處理的所有 URL。
更新應用程式進入點
主控台應用程式的主要進入點是 Main
方法。 以下列內容取代現有的方法:
static Task Main() => SumPageSizesAsync();
更新的 Main
方法現在會視為 Async main,其允許可執行檔中的非同步進入點。 其會表示為 SumPageSizesAsync
的呼叫。
建立非同步總和頁面大小方法
在 Main
方法下方,新增 SumPageSizesAsync
方法:
static async Task SumPageSizesAsync()
{
var stopwatch = Stopwatch.StartNew();
IEnumerable<Task<int>> downloadTasksQuery =
from url in s_urlList
select ProcessUrlAsync(url, s_client);
List<Task<int>> downloadTasks = downloadTasksQuery.ToList();
int total = 0;
while (downloadTasks.Any())
{
Task<int> finishedTask = await Task.WhenAny(downloadTasks);
downloadTasks.Remove(finishedTask);
total += await finishedTask;
}
stopwatch.Stop();
Console.WriteLine($"\nTotal bytes returned: {total:#,#}");
Console.WriteLine($"Elapsed time: {stopwatch.Elapsed}\n");
}
while
迴圈會移除每個反覆項目其中一個工作。 完成每項工作之後,迴圈就會結束。 此方法會藉由將 Stopwatch 具現化並加以啟動來啟動。 然後該方法包括在執行時會建立一組工作的查詢。 下列程式碼中的每個 ProcessUrlAsync
呼叫都會傳回 TResult
為整數的 Task<TResult>:
IEnumerable<Task<int>> downloadTasksQuery =
from url in s_urlList
select ProcessUrlAsync(url, s_client);
由於 LINQ 延後執行,因此您會呼叫 Enumerable.ToList 來啟動每個工作。
List<Task<int>> downloadTasks = downloadTasksQuery.ToList();
while
迴圈會對集合中的每個工作執行下列步驟:
等候
WhenAny
呼叫,以識別集合中的第一個工作已完成其下載。Task<int> finishedTask = await Task.WhenAny(downloadTasks);
從集合中移除該工作。
downloadTasks.Remove(finishedTask);
等候
ProcessUrlAsync
呼叫所傳回的finishedTask
。finishedTask
變數是 Task<TResult>,其中TResult
是整數。 工作已完成,但您等候它擷取所下載網站的長度,如下列範例所示。 若工作發生錯誤,則await
將會擲回儲存於AggregateException
中的第一個子例外狀況,與讀取 Task<TResult>.Result 屬性不同,這會擲回AggregateException
。total += await finishedTask;
新增處理方法
在 SumPageSizesAsync
方法下方,新增下列 ProcessUrlAsync
方法:
static async Task<int> ProcessUrlAsync(string url, HttpClient client)
{
byte[] content = await client.GetByteArrayAsync(url);
Console.WriteLine($"{url,-60} {content.Length,10:#,#}");
return content.Length;
}
對於任何指定的 URL,此方法會使用提供的 client
執行個體,以取得作為 byte[]
的回應。 長度會在 URL 之後傳回,而長度會寫入主控台。
執行程式數次,確認所下載的長度不一定會以相同的順序出現。
警告
您可以如這個範例所述在迴圈中使用 WhenAny
,以解決牽涉到少量工作的問題。 不過,如果您有大量的工作要處理,其他方法會更有效率。 如需詳細資訊和範例,請參閱 Processing tasks as they complete (在工作完成時加以處理)。
完整範例
下列程式碼是範例 Program.cs 檔案的完整文字。
using System.Diagnostics;
HttpClient s_client = new()
{
MaxResponseContentBufferSize = 1_000_000
};
IEnumerable<string> s_urlList = new string[]
{
"https://learn.microsoft.com",
"https://learn.microsoft.com/aspnet/core",
"https://learn.microsoft.com/azure",
"https://learn.microsoft.com/azure/devops",
"https://learn.microsoft.com/dotnet",
"https://learn.microsoft.com/dynamics365",
"https://learn.microsoft.com/education",
"https://learn.microsoft.com/enterprise-mobility-security",
"https://learn.microsoft.com/gaming",
"https://learn.microsoft.com/graph",
"https://learn.microsoft.com/microsoft-365",
"https://learn.microsoft.com/office",
"https://learn.microsoft.com/powershell",
"https://learn.microsoft.com/sql",
"https://learn.microsoft.com/surface",
"https://learn.microsoft.com/system-center",
"https://learn.microsoft.com/visualstudio",
"https://learn.microsoft.com/windows",
"https://learn.microsoft.com/maui"
};
await SumPageSizesAsync();
async Task SumPageSizesAsync()
{
var stopwatch = Stopwatch.StartNew();
IEnumerable<Task<int>> downloadTasksQuery =
from url in s_urlList
select ProcessUrlAsync(url, s_client);
List<Task<int>> downloadTasks = downloadTasksQuery.ToList();
int total = 0;
while (downloadTasks.Any())
{
Task<int> finishedTask = await Task.WhenAny(downloadTasks);
downloadTasks.Remove(finishedTask);
total += await finishedTask;
}
stopwatch.Stop();
Console.WriteLine($"\nTotal bytes returned: {total:#,#}");
Console.WriteLine($"Elapsed time: {stopwatch.Elapsed}\n");
}
static async Task<int> ProcessUrlAsync(string url, HttpClient client)
{
byte[] content = await client.GetByteArrayAsync(url);
Console.WriteLine($"{url,-60} {content.Length,10:#,#}");
return content.Length;
}
// Example output:
// https://learn.microsoft.com 132,517
// https://learn.microsoft.com/powershell 57,375
// https://learn.microsoft.com/gaming 33,549
// https://learn.microsoft.com/aspnet/core 88,714
// https://learn.microsoft.com/surface 39,840
// https://learn.microsoft.com/enterprise-mobility-security 30,903
// https://learn.microsoft.com/microsoft-365 67,867
// https://learn.microsoft.com/windows 26,816
// https://learn.microsoft.com/maui 57,958
// https://learn.microsoft.com/dotnet 78,706
// https://learn.microsoft.com/graph 48,277
// https://learn.microsoft.com/dynamics365 49,042
// https://learn.microsoft.com/office 67,867
// https://learn.microsoft.com/system-center 42,887
// https://learn.microsoft.com/education 38,636
// https://learn.microsoft.com/azure 421,663
// https://learn.microsoft.com/visualstudio 30,925
// https://learn.microsoft.com/sql 54,608
// https://learn.microsoft.com/azure/devops 86,034
// Total bytes returned: 1,454,184
// Elapsed time: 00:00:01.1290403