Compartir a través de


Cancelar una lista de tareas

Puede cancelar una aplicación de consola asincrónica si no quiere esperar a que termine. Mediante el ejemplo de este tema, puede agregar una cancelación a una aplicación que descargue el contenido de una lista de sitios web. Puede cancelar muchas tareas asociando la instancia de CancellationTokenSource a cada tarea. Si se presiona la tecla Entrar, se cancelan todas las tareas que aún no se han completado.

Esta tutorial abarca lo siguiente:

  • Creación de una aplicación de consola de .NET
  • Escritura de una aplicación asincrónica que admite la cancelación
  • Demostración de la señalización de una cancelación

Requisitos previos

Este tutorial requiere lo siguiente:

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 su editor de código favorito.

Reemplazo de instrucciones using

Reemplace las instrucciones using existentes por estas declaraciones:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

Adición de campos

Dentro de la definición de la clase Program, agregue estos tres campos:

static readonly CancellationTokenSource s_cts = new CancellationTokenSource();

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"
};

CancellationTokenSource se usa para indicar una cancelación solicitada a un token de cancelación (CancellationToken). 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 async Task Main()
{
    Console.WriteLine("Application started.");
    Console.WriteLine("Press the ENTER key to cancel...\n");

    Task cancelTask = Task.Run(() =>
    {
        while (Console.ReadKey().Key != ConsoleKey.Enter)
        {
            Console.WriteLine("Press the ENTER key to cancel...");
        }

        Console.WriteLine("\nENTER key pressed: cancelling downloads.\n");
        s_cts.Cancel();
    });
    
    Task sumPageSizesTask = SumPageSizesAsync();

    Task finishedTask = await Task.WhenAny(new[] { cancelTask, sumPageSizesTask });
    if (finishedTask == cancelTask)
    {
        // wait for the cancellation to take place:
        try
        {
            await sumPageSizesTask;
            Console.WriteLine("Download task completed before cancel request was processed.");
        }
        catch (TaskCanceledException)
        {
            Console.WriteLine("Download task has been cancelled.");
        }
    }
        
    Console.WriteLine("Application ending.");
}

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. Escribe algunos mensajes informativos en la consola y, luego, declara una instancia de Task denominada cancelTask, la cual leerá las pulsaciones de teclas de la consola. Si se presiona la tecla Entrar, se realiza una llamada a CancellationTokenSource.Cancel(). Esto indicará la cancelación. Después, se asigna la variable sumPageSizesTask desde el método SumPageSizesAsync y ambas tareas se pasan a Task.WhenAny(Task[]), que continuará cuando se complete cualquiera de las dos tareas.

El siguiente bloque de código garantiza que la aplicación no salga hasta que se haya procesado la cancelación. Si la primera tarea que se va a completar es cancelTask, se suspende sumPageSizeTask con await. Si se ha cancelado, cuando se esperaba que se hubiera suspendido con await, produce una excepción System.Threading.Tasks.TaskCanceledException. El bloque detecta esa excepción e imprime un mensaje.

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();

    int total = 0;
    foreach (string url in s_urlList)
    {
        int contentLength = await ProcessUrlAsync(url, s_client, s_cts.Token);
        total += contentLength;
    }

    stopwatch.Stop();

    Console.WriteLine($"\nTotal bytes returned:  {total:#,#}");
    Console.WriteLine($"Elapsed time:          {stopwatch.Elapsed}\n");
}

El método comienza creando una instancia e iniciando una clase Stopwatch. Luego, recorre en bucle cada dirección URL en s_urlList y llama a ProcessUrlAsync. Con cada iteración, se pasa el token s_cts.Token al método ProcessUrlAsync y el código devuelve una clase Task<TResult>, donde TResult es un entero:

int total = 0;
foreach (string url in s_urlList)
{
    int contentLength = await ProcessUrlAsync(url, s_client, s_cts.Token);
    total += contentLength;
}

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, CancellationToken token)
{
    HttpResponseMessage response = await client.GetAsync(url, token);
    byte[] content = await response.Content.ReadAsByteArrayAsync(token);
    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 instancia de CancellationToken se pasa a los métodos HttpClient.GetAsync(String, CancellationToken) y HttpContent.ReadAsByteArrayAsync(). El token (token) se usa para registrar la cancelación solicitada. La longitud se devuelve después de que la dirección URL y la longitud se escriban en la consola.

Ejemplo de resultado de la aplicación

Application started.
Press the ENTER key to cancel...

https://learn.microsoft.com                                       37,357
https://learn.microsoft.com/aspnet/core                           85,589
https://learn.microsoft.com/azure                                398,939
https://learn.microsoft.com/azure/devops                          73,663
https://learn.microsoft.com/dotnet                                67,452
https://learn.microsoft.com/dynamics365                           48,582
https://learn.microsoft.com/education                             22,924

ENTER key pressed: cancelling downloads.

Application ending.

Ejemplo completo

El código siguiente es el texto completo del archivo Program.cs para el ejemplo.

using System.Diagnostics;

class Program
{
    static readonly CancellationTokenSource s_cts = new CancellationTokenSource();

    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"
    };

    static async Task Main()
    {
        Console.WriteLine("Application started.");
        Console.WriteLine("Press the ENTER key to cancel...\n");

        Task cancelTask = Task.Run(() =>
        {
            while (Console.ReadKey().Key != ConsoleKey.Enter)
            {
                Console.WriteLine("Press the ENTER key to cancel...");
            }

            Console.WriteLine("\nENTER key pressed: cancelling downloads.\n");
            s_cts.Cancel();
        });

        Task sumPageSizesTask = SumPageSizesAsync();

        Task finishedTask = await Task.WhenAny(new[] { cancelTask, sumPageSizesTask });
        if (finishedTask == cancelTask)
        {
            // wait for the cancellation to take place:
            try
            {
                await sumPageSizesTask;
                Console.WriteLine("Download task completed before cancel request was processed.");
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("Download task has been cancelled.");
            }
        }

        Console.WriteLine("Application ending.");
    }

    static async Task SumPageSizesAsync()
    {
        var stopwatch = Stopwatch.StartNew();

        int total = 0;
        foreach (string url in s_urlList)
        {
            int contentLength = await ProcessUrlAsync(url, s_client, s_cts.Token);
            total += contentLength;
        }

        stopwatch.Stop();

        Console.WriteLine($"\nTotal bytes returned:  {total:#,#}");
        Console.WriteLine($"Elapsed time:          {stopwatch.Elapsed}\n");
    }

    static async Task<int> ProcessUrlAsync(string url, HttpClient client, CancellationToken token)
    {
        HttpResponseMessage response = await client.GetAsync(url, token);
        byte[] content = await response.Content.ReadAsByteArrayAsync(token);
        Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

        return content.Length;
    }
}

Vea también

Pasos siguientes