Обработка асинхронных задач по мере завершения (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
выполняет следующие действия для каждой задачи в коллекции.
Ожидает вызов
WhenAny
для определения первой задачи в коллекции, чтобы завершить ее загрузку.Task<int> finishedTask = await Task.WhenAny(downloadTasks);
Удаляет эту задачу из коллекции.
downloadTasks.Remove(finishedTask);
Ожидает
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