Compartir a través de


Procesar tareas asincrónicas a medida que se completan (C#)

Task.WhenAnyMediante , puede iniciar varias tareas al mismo tiempo y procesarlas una por una a medida que se completan en lugar de procesarlas en el orden en que se inician.

En el ejemplo siguiente 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 esperada a WhenAny devuelve la tarea en la colección de tareas que finaliza su descarga en primer lugar. 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 siguientes opciones:

  • 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.
  • El SDK de .NET con un editor de código que prefiera, como Visual Studio Code.

Creación de una aplicación de ejemplo

Cree una nueva aplicación de consola de .NET Core. Puede crear uno 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 código:

using System.Diagnostics;

namespace ProcessTasksAsTheyFinish;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

Adición de campos

En la definición de 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 recibir respuestas HTTP. s_urlList contiene todas las direcciones URL que la aplicación planea procesar.

Actualizar el punto de entrada de la aplicación

El punto de entrada principal en la aplicación de consola es el Main método . Reemplace el método existente por lo siguiente:

static Task Main() => SumPageSizesAsync();

El método actualizado Main ahora se considera un elemento principal asincrónico, que permite un punto de entrada asincrónico en el ejecutable. Se expresa como una llamada a SumPageSizesAsync.

Creación del método de tamaños de página de suma asincrónica

Debajo del Main método , agregue el SumPageSizesAsync método :

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 while bucle quita una de las tareas de cada iteración. Una vez completada cada tarea, el bucle finaliza. El método comienza creando instancias e iniciando un Stopwatch. A continuación, incluye una consulta que, cuando se ejecuta, crea una colección de tareas. Cada llamada a ProcessUrlAsync en el código siguiente devuelve un 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 Enumerable.ToList a para iniciar cada tarea.

List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

El while bucle realiza los pasos siguientes para cada tarea de la colección:

  1. Espera una llamada a para WhenAny identificar la primera tarea de la colección que ha finalizado su descarga.

    Task<int> finishedTask = await Task.WhenAny(downloadTasks);
    
  2. Quita esa tarea de la colección.

    downloadTasks.Remove(finishedTask);
    
  3. finishedTaskEspera , que devuelve una llamada a ProcessUrlAsync. La finishedTask variable es un Task<TResult> donde TResult es un entero. La tarea ya está completa, pero espera que recupere la longitud del sitio web descargado, como se muestra en el ejemplo siguiente. Si se produce un error en la tarea, await se producirá la primera excepción secundaria almacenada en AggregateException, a diferencia de leer la Task<TResult>.Result propiedad , que produciría .AggregateException

    total += await finishedTask;
    

Agregar método de proceso

Agregue el método siguiente ProcessUrlAsync debajo del SumPageSizesAsync método :

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 determinada, el método usará la client instancia proporcionada para obtener la respuesta como .byte[] La longitud se devuelve después de escribir la dirección URL y la longitud en la consola.

Ejecute el programa varias veces para comprobar que las longitudes descargadas no siempre aparecen en el mismo orden.

Precaución

Puede usar WhenAny en un bucle, como se describe en el ejemplo, para resolver problemas que implican un pequeño número de tareas. Sin embargo, otros enfoques son más eficaces si tiene un gran número de tareas que procesar. Para obtener más información y ejemplos, vea Procesamiento de tareas a medida que se completan.

Simplificación del enfoque mediante Task.WhenEach

El while bucle implementado en SumPageSizesAsync el método se puede simplificar mediante el nuevo Task.WhenEach método introducido en .NET 9 llamando a él en await foreach bucle.
Reemplace el bucle implementado while anteriormente:

    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        total += await finishedTask;
    }

con el simplificado await foreach:

    await foreach (Task<int> t in Task.WhenEach(downloadTasks))
    {
        total += await t;
    }

Este nuevo enfoque ya no permite llamar Task.WhenAny repetidamente a para llamar manualmente a una tarea y quitar el que finaliza, ya Task.WhenEach que recorre en iteración la tarea en un orden de su finalización.

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

Consulte también