通过使用 Task.WhenAny,可同时启动多个任务,并在它们完成时逐个对它们进行处理,而不是按照它们的启动顺序进行处理。
下面的示例使用查询来创建一组任务。 每个任务都下载指定网站的内容。 在对 while 循环的每次迭代中,对 WhenAny 的等待调用返回任务集合中首先完成下载的任务。 此任务从集合中删除并进行处理。 循环重复进行,直到集合中不包含任何任务。
创建示例应用程序
创建新的 .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 方法视为异步 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 进行调用都会返回 Task<TResult>,其中 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
循环针对集合中的每个任务执行以下步骤:
// 1. 等待调用 WhenAny,以标识集合中首个已完成下载的任务。
Task<int> finishedTask = await Task.WhenAny(downloadTasks);
// 2. 从集合中移除任务。
downloadTasks.Remove(finishedTask);
// 3. 等待 finishedTask,由对 ProcessUrlAsync 的调用返回。
// 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 来解决涉及少量任务的问题。 但是,如果要处理大量任务,可以采用其他更高效的方法。
完整示例
下列代码是示例的 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