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


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

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

В следующем примере используется запрос для создания коллекции задач. Каждая задача загружает содержимое указанного веб-сайта. В каждой итерации цикла while ожидаемый вызов 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 теперь считается асинхронным методом 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;
    

Добавление метода обработки

Добавьте приведенный ниже метод 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-адрес и длина записываются в консоль.

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

Внимание!

Можно использовать 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

См. также раздел