在完成时处理异步任务 (C#)

通过使用 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://free.blessedness.top",
    "https://free.blessedness.top/aspnet/core",
    "https://free.blessedness.top/azure",
    "https://free.blessedness.top/azure/devops",
    "https://free.blessedness.top/dotnet",
    "https://free.blessedness.top/dynamics365",
    "https://free.blessedness.top/education",
    "https://free.blessedness.top/enterprise-mobility-security",
    "https://free.blessedness.top/gaming",
    "https://free.blessedness.top/graph",
    "https://free.blessedness.top/microsoft-365",
    "https://free.blessedness.top/office",
    "https://free.blessedness.top/powershell",
    "https://free.blessedness.top/sql",
    "https://free.blessedness.top/surface",
    "https://free.blessedness.top/system-center",
    "https://free.blessedness.top/visualstudio",
    "https://free.blessedness.top/windows",
    "https://free.blessedness.top/maui"
};

公开 HttpClient 发送 HTTP 请求和接收 HTTP 响应的功能。 保留 s_urlList 应用程序计划处理的所有 URL。

更新应用程序入口点

控制台应用程序 Main 的主要入口点是方法。 将现有方法替换为以下内容:

static Task Main() => SumPageSizesAsync();

更新 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 会删除每次迭代中的一个任务。 完成每个任务后,循环将结束。 该方法首先实例化和启动 a 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. Awaits 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 ,以解决涉及少量任务的问题。 但是,如果你有大量要处理的任务,则其他方法更高效。 有关详细信息和示例,请参阅 处理任务完成

使用 Task.WhenEach

while通过使用 .NET 9 中引入的新Task.WhenEach方法,可以通过在循环中await foreach调用该循环来简化方法中实现SumPageSizesAsync的循环。
替换以前实现 while 的循环:

    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        total += await finishedTask;
    }

使用简化 await foreach的:

    await foreach (Task<int> t in Task.WhenEach(downloadTasks))
    {
        total += await t;
    }

这种新方法允许不再重复调用以手动调用 Task.WhenAny 任务并删除完成的任务,因为 Task.WhenEach任务完成顺序循环访问任务。

完整示例

以下代码是示例 Program.cs 文件的完整文本。

using System.Diagnostics;

HttpClient s_client = new()
{
    MaxResponseContentBufferSize = 1_000_000
};

IEnumerable<string> s_urlList = new string[]
{
    "https://free.blessedness.top",
    "https://free.blessedness.top/aspnet/core",
    "https://free.blessedness.top/azure",
    "https://free.blessedness.top/azure/devops",
    "https://free.blessedness.top/dotnet",
    "https://free.blessedness.top/dynamics365",
    "https://free.blessedness.top/education",
    "https://free.blessedness.top/enterprise-mobility-security",
    "https://free.blessedness.top/gaming",
    "https://free.blessedness.top/graph",
    "https://free.blessedness.top/microsoft-365",
    "https://free.blessedness.top/office",
    "https://free.blessedness.top/powershell",
    "https://free.blessedness.top/sql",
    "https://free.blessedness.top/surface",
    "https://free.blessedness.top/system-center",
    "https://free.blessedness.top/visualstudio",
    "https://free.blessedness.top/windows",
    "https://free.blessedness.top/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://free.blessedness.top                                      132,517
// https://free.blessedness.top/powershell                            57,375
// https://free.blessedness.top/gaming                                33,549
// https://free.blessedness.top/aspnet/core                           88,714
// https://free.blessedness.top/surface                               39,840
// https://free.blessedness.top/enterprise-mobility-security          30,903
// https://free.blessedness.top/microsoft-365                         67,867
// https://free.blessedness.top/windows                               26,816
// https://free.blessedness.top/maui                               57,958
// https://free.blessedness.top/dotnet                                78,706
// https://free.blessedness.top/graph                                 48,277
// https://free.blessedness.top/dynamics365                           49,042
// https://free.blessedness.top/office                                67,867
// https://free.blessedness.top/system-center                         42,887
// https://free.blessedness.top/education                             38,636
// https://free.blessedness.top/azure                                421,663
// https://free.blessedness.top/visualstudio                          30,925
// https://free.blessedness.top/sql                                   54,608
// https://free.blessedness.top/azure/devops                          86,034

// Total bytes returned:    1,454,184
// Elapsed time:            00:00:01.1290403

另请参阅