Iniciar varias tareas asincrónicas y procesarlas a medida que se completan (C#)
Si usa Task.WhenAny, puede iniciar varias tareas a la vez y procesarlas una por una a medida que se completen, en lugar de procesarlas en el orden en el que se han iniciado.
En el siguiente ejemplo se usa una consulta para crear una colección de tareas. Cada tarea descarga el contenido de un sitio web especificado. En cada iteración de un bucle while, una llamada awaited a WhenAny devuelve la tarea en la colección de tareas que termine primero su descarga. Esa tarea se quita de la colección y se procesa. El bucle se repite hasta que la colección no contiene más tareas.
Prerrequisitos
Puede seguir este tutorial mediante una de las opciones siguientes:
- Visual Studio 2022 con la carga de trabajo de desarrollo de escritorio de .NET instalada. El SDK de .NET se instala automáticamente al seleccionar esta carga de trabajo.
- SDK de .NET con un editor de código de su elección, como Visual Studio Code.
Creación de una aplicación de ejemplo
Cree una nueva aplicación de consola de .NET Core. Puede crear una mediante el comando dotnet new console o desde Visual Studio.
Abra el archivo Program.cs en el editor de código y reemplace el código existente por este:
using System.Diagnostics;
namespace ProcessTasksAsTheyFinish;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
Adición de campos
Dentro de la definición de la clase Program
, agregue los dos campos siguientes:
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
expone la capacidad de enviar solicitudes HTTP y de recibir respuestas HTTP. s_urlList
contiene todas las direcciones URL que planea procesar la aplicación.
Actualización del punto de entrada de la aplicación
El punto de entrada principal de la aplicación de consola es el método Main
. Reemplace el método existente por lo siguiente:
static Task Main() => SumPageSizesAsync();
El método Main
actualizado ahora se considera un método Async main, el cual permite un punto de entrada asincrónico en el archivo ejecutable. Se expresa como una llamada a SumPageSizesAsync
.
Creación de un método SumPageSizes asincrónico
Agregue el método SumPageSizesAsync
después del método Main
:
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");
}
El bucle while
quita una de las tareas de cada iteración. Una vez completadas todas las tareas, el bucle finaliza. El método comienza creando una instancia e iniciando una clase Stopwatch. Después, incluye una consulta que, cuando se ejecuta, crea una colección de tareas. Cada llamada a ProcessUrlAsync
en el siguiente código devuelve un objeto Task<TResult>, donde TResult
es un entero:
IEnumerable<Task<int>> downloadTasksQuery =
from url in s_urlList
select ProcessUrlAsync(url, s_client);
Debido a la ejecución diferida con LINQ, se llama a Enumerable.ToList para iniciar cada tarea.
List<Task<int>> downloadTasks = downloadTasksQuery.ToList();
El bucle while
realiza los pasos siguientes para cada tarea de la colección:
Espera una llamada a
WhenAny
para identificar la primera tarea de la colección que ha finalizado su descarga.Task<int> finishedTask = await Task.WhenAny(downloadTasks);
Quita la tarea de la colección.
downloadTasks.Remove(finishedTask);
Espera
finishedTask
, que se devuelve mediante una llamada aProcessUrlAsync
. La variablefinishedTask
es un Task<TResult> dondeTResult
es un entero. La tarea ya está completa, pero la espera para recuperar la longitud del sitio web descargado, como se muestra en el ejemplo siguiente. Si se produce un error en la tarea,await
iniciará la primera excepción secundaria almacenada enAggregateException
, en lugar de leer la propiedad Task<TResult>.Result que iniciaría la excepciónAggregateException
.total += await finishedTask;
Adición de un método de proceso
Agregue el siguiente método ProcessUrlAsync
después del método 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;
}
Para cualquier dirección URL, el método usará la instancia de client
proporcionada para obtener la respuesta como byte[]
. La longitud se devuelve después de que la dirección URL y la longitud se escriban en la consola.
Ejecute el programa varias veces para comprobar que las longitudes que se han descargado no aparecen siempre en el mismo orden.
Precaución
Puede usar WhenAny
en un bucle, como se describe en el ejemplo, para solucionar problemas que implican un número reducido de tareas. Sin embargo, otros enfoques son más eficaces si hay que procesar un gran número de tareas. Para más información y ejemplos, vea Processing Tasks as they complete (Procesar tareas a medida que se completan).
Ejemplo completo
El código siguiente es el texto completo del archivo Program.cs para el ejemplo.
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