在异步任务完成时对其进行处理 (C#)

通过使用 Task.WhenAny,可同时启动多个任务,并在它们完成时逐个对它们进行处理,而不是按照它们的启动顺序进行处理。

下面的示例使用查询来创建一组任务。 每个任务都下载指定网站的内容。 在对 while 循环的每次迭代中,对 WhenAny 的等待调用返回任务集合中首先完成下载的任务。 此任务从集合中删除并进行处理。 循环重复进行,直到集合中不包含任何任务。

先决条件

可以通过以下选项之一来学习本教程:

创建示例应用程序

创建一个面向 .NET 6.0 的新 .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/xamarin"
};

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");
}

该方法从实例化和启动 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 来解决涉及少量任务的问题。 但是,如果要处理大量任务,可以采用其他更高效的方法。 有关详细信息和示例,请参阅 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://docs.microsoft.com",
    "https://docs.microsoft.com/aspnet/core",
    "https://docs.microsoft.com/azure",
    "https://docs.microsoft.com/azure/devops",
    "https://docs.microsoft.com/dotnet",
    "https://docs.microsoft.com/dynamics365",
    "https://docs.microsoft.com/education",
    "https://docs.microsoft.com/enterprise-mobility-security",
    "https://docs.microsoft.com/gaming",
    "https://docs.microsoft.com/graph",
    "https://docs.microsoft.com/microsoft-365",
    "https://docs.microsoft.com/office",
    "https://docs.microsoft.com/powershell",
    "https://docs.microsoft.com/sql",
    "https://docs.microsoft.com/surface",
    "https://docs.microsoft.com/system-center",
    "https://docs.microsoft.com/visualstudio",
    "https://docs.microsoft.com/windows",
    "https://docs.microsoft.com/xamarin"
};

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

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

另请参阅

通过使用 Task.WhenAny,可同时启动多个任务,并在它们完成时逐个对它们进行处理,而不是按照它们的启动顺序进行处理。

下面的示例使用查询来创建一组任务。 每个任务都下载指定网站的内容。 在对 while 循环的每次迭代中,对 WhenAny 的等待调用返回任务集合中首先完成下载的任务。 此任务从集合中删除并进行处理。 循环重复进行,直到集合中不包含任何任务。

先决条件

可以通过以下选项之一来学习本教程:

创建示例应用程序

创建面向 .NET 5.0 或 .NET Core 3.1 的新 .NET Core 控制台应用程序。 可使用 dotnet new console 命令或 Visual Studio 进行创建。 在你最喜欢的编辑器中打开 Program.cs 文件。

替换 using 语句

将现有 using 语句替换为以下声明:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

添加字段

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/xamarin"
};

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");
}

该方法从实例化和启动 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 来解决涉及少量任务的问题。 但是,如果要处理大量任务,可以采用其他更高效的方法。 有关详细信息和示例,请参阅 Processing tasks as they complete(在任务完成时处理它们)。

完整示例

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

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

namespace ProcessTasksAsTheyFinish
{
    class Program
    {
        static readonly HttpClient s_client = new HttpClient
        {
            MaxResponseContentBufferSize = 1_000_000
        };

        static readonly IEnumerable<string> s_urlList = new string[]
        {
            "https://docs.microsoft.com",
            "https://docs.microsoft.com/aspnet/core",
            "https://docs.microsoft.com/azure",
            "https://docs.microsoft.com/azure/devops",
            "https://docs.microsoft.com/dotnet",
            "https://docs.microsoft.com/dynamics365",
            "https://docs.microsoft.com/education",
            "https://docs.microsoft.com/enterprise-mobility-security",
            "https://docs.microsoft.com/gaming",
            "https://docs.microsoft.com/graph",
            "https://docs.microsoft.com/microsoft-365",
            "https://docs.microsoft.com/office",
            "https://docs.microsoft.com/powershell",
            "https://docs.microsoft.com/sql",
            "https://docs.microsoft.com/surface",
            "https://docs.microsoft.com/system-center",
            "https://docs.microsoft.com/visualstudio",
            "https://docs.microsoft.com/windows",
            "https://docs.microsoft.com/xamarin"
        };

        static Task 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");
        }

        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://docs.microsoft.com/windows                               25,513
// https://docs.microsoft.com/gaming                                30,705
// https://docs.microsoft.com/dotnet                                69,626
// https://docs.microsoft.com/dynamics365                           50,756
// https://docs.microsoft.com/surface                               35,519
// https://docs.microsoft.com                                       39,531
// https://docs.microsoft.com/azure/devops                          75,837
// https://docs.microsoft.com/xamarin                               60,284
// https://docs.microsoft.com/system-center                         43,444
// https://docs.microsoft.com/enterprise-mobility-security          28,946
// https://docs.microsoft.com/microsoft-365                         43,278
// https://docs.microsoft.com/visualstudio                          31,414
// https://docs.microsoft.com/office                                42,292
// https://docs.microsoft.com/azure                                401,113
// https://docs.microsoft.com/graph                                 46,831
// https://docs.microsoft.com/education                             25,098
// https://docs.microsoft.com/powershell                            58,173
// https://docs.microsoft.com/aspnet/core                           87,763
// https://docs.microsoft.com/sql                                   53,362
   
// Total bytes returned: 1,249,485
// Elapsed time:          00:00:02.7068725

另请参阅