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:
- .NET 5 o un SDK posterior
- Entorno de desarrollo integrado (IDE)
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;
}
}