Поделиться через


Обработка асинхронных задач по мере их выполнения (C#)

С помощью Task.WhenAnyможно одновременно запускать несколько задач и обрабатывать их по одному, а не обрабатывать их в том порядке, в котором они запущены.

В следующем примере используется запрос для создания коллекции задач. Каждая задача скачивает содержимое указанного веб-сайта. В каждой итерации цикла во время ожидания вызов WhenAny возвращает задачу в коллекции задач, завершающих загрузку первым. Эта задача удаляется из коллекции и обрабатывается. Цикл повторяется, пока коллекция не содержит больше задач.

Предпосылки

Вы можете следовать этому руководству, используя один из следующих вариантов:

  • Visual Studio 2022 с установленным компонентом разработки десктопных приложений .NET. Пакет SDK для .NET устанавливается автоматически при выборе этой рабочей нагрузки.
  • Пакет SDK для .NET с выбранным редактором кода, например 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 метод теперь считается асинхронным основным, что позволяет асинхронной точке входа в исполняемый файл. Он выражается как призыв к SumPageSizesAsync.

Создание метода асинхронных размеров страниц суммы

Под методом MainSumPageSizesAsync добавьте метод:

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. Awaits finishedTask, который возвращается вызовом ProcessUrlAsync. Переменная finishedTask — это Task<TResult>TResult целое число. Задача уже завершена, но вы ожидаете, чтобы получить длину скачаемого веб-сайта, как показано в следующем примере. Если задача неисправна, await вызовет первое дочернее исключение, хранящееся в AggregateExceptionсвойстве, в отличие от чтения Task<TResult>.Result свойства, которое вызовет исключение AggregateException.

    total += await finishedTask;
    

Добавление метода процесса

Добавьте следующий ProcessUrlAsync метод под методом SumPageSizesAsync :

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-адреса и длины в консоль.

Запустите программу несколько раз, чтобы убедиться, что скачанные длины не всегда отображаются в одном порядке.

Caution

Вы можете использовать WhenAny цикл, как описано в примере, для решения проблем, связанных с небольшим количеством задач. Однако другие подходы более эффективны при наличии большого количества задач для обработки. Дополнительные сведения и примеры см. в разделе "Обработка задач по мере их выполнения".

Упрощение подхода Task.WhenEach

Циклwhile, реализованный в методе, можно упростить с помощью нового Task.WhenEach метода, представленного в SumPageSizesAsync .NET 9, вызвав его в await foreach цикле.
Замените ранее реализованный 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://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

См. также