Una guía del programador de .NET para CancellationToken

Publicado: 2022-08-23

A veces cancelar es algo bueno. En muchos de mis proyectos .NET, he tenido mucha motivación para cancelar procesos tanto internos como externos. Microsoft se enteró de que los desarrolladores estaban abordando este caso de uso común en una variedad de implementaciones complejas y decidió que debía haber una mejor manera. Por lo tanto, se introdujo un patrón de comunicación de cancelación común como CancellationToken , que se creó utilizando construcciones de comunicación entre procesos y subprocesos múltiples de nivel inferior. Como parte de mi investigación inicial sobre este patrón, y después de haber explorado el código fuente real de .NET para la implementación de Microsoft, descubrí que CancellationToken puede resolver un conjunto de problemas mucho más amplio: suscripciones en estados de ejecución de aplicaciones, operaciones de tiempo de espera usando diferentes disparadores y comunicaciones generales entre procesos a través de banderas.

El caso de uso del token de cancelación previsto

CancellationToken se introdujo en .NET 4 como un medio para mejorar y estandarizar las soluciones existentes para cancelar operaciones. Hay cuatro enfoques generales para manejar la cancelación que los lenguajes de programación populares tienden a implementar:

Matar Dile, no aceptes un no por respuesta Pregunte cortésmente y acepte el rechazo. Establezca la bandera cortésmente, déjelo sondear si quiere
Acercarse Parada fuerte; resolver las inconsistencias más tarde Dile que se detenga, pero deja que limpie las cosas. Una solicitud directa pero gentil para detener Pídele que se detenga, pero no lo fuerces.
Resumen Un camino seguro hacia la corrupción y el dolor Permite puntos de parada limpios pero debe detenerse Permite puntos de parada limpios, pero la solicitud de cancelación puede ignorarse Se solicita la cancelación a través de una bandera
subprocesos pthread_kill ,
pthread_cancel (asincrónico)
pthread_cancel (modo diferido) n / A A través de una bandera
.RED Thread.Abort n / A Thread.Interrupt A través de una bandera en CancellationToken
Java Thread.destroy ,
Thread.stop
n / A Thread.interrupt A través de una bandera o Thread.interrupted
Pitón PyThreadState_SetAsyncExc n / A asyncio.Task.cancel A través de una bandera
Guía Inaceptable; evitar este enfoque Aceptable, especialmente cuando un idioma no admite excepciones o desconexión Aceptable si el idioma lo admite Mejor, pero más como un esfuerzo de grupo.
Resumen del enfoque de cancelación y ejemplos de lenguaje

CancellationToken reside en la categoría final, donde la conversación de cancelación es cooperativa.

Después de que Microsoft introdujera CancellationToken , la comunidad de desarrollo lo adoptó rápidamente, especialmente porque muchas de las principales API de .NET se actualizaron para usar estos tokens de forma nativa. Por ejemplo, a partir de ASP.NET Core 2.0, las acciones admiten un parámetro CancellationToken opcional que puede indicar si se cerró una solicitud HTTP, lo que permite la cancelación de cualquier operación y, por lo tanto, evita el uso innecesario de recursos.

Después de una inmersión profunda en el código base de .NET, quedó claro que el uso de CancellationToken no se limita a la cancelación.

CancellationToken bajo un microscopio

Al observar más de cerca la implementación de CancellationToken , vemos que es solo un indicador simple (es decir, ManualResetEvent ) y la infraestructura de soporte que brinda la capacidad de monitorear y cambiar ese indicador. La utilidad principal de CancellationToken está en su nombre, lo que sugiere que esta es la forma común de cancelar operaciones. Hoy en día, cualquier biblioteca, paquete o marco .NET con operaciones asincrónicas o de larga ejecución permite la cancelación a través de estos tokens.

CancellationToken puede activarse configurando manualmente su indicador en "verdadero" o programándolo para que cambie a "verdadero" después de que haya transcurrido un cierto período de tiempo. Independientemente de cómo se active un CancellationToken , el código de cliente que supervisa este token puede determinar el valor del indicador del token a través de uno de estos tres métodos:

  • Usando un WaitHandle
  • Sondeo de la bandera de CancellationToken
  • Informar al código del cliente cuando se actualiza el estado de la bandera a través de una suscripción programática

Después de una mayor investigación en el código base de .NET, se hizo evidente que el equipo de .NET encontró que CancellationTokens era útil en otros escenarios no relacionados con la cancelación. Exploremos algunos de estos casos de uso avanzados y fuera de marca, que capacitan a los desarrolladores de C# con coordinación entre procesos y subprocesos múltiples para simplificar situaciones complejas.

CancellationTokens para eventos avanzados

Al escribir aplicaciones ASP.NET Core, a veces necesitamos saber cuándo se inició nuestra aplicación o necesitamos inyectar nuestro código en el proceso de apagado del host. En esos casos, usamos la interfaz IHostApplicationLifetime (anteriormente IApplicationLifetime ). Esta interfaz (del repositorio de .NET Core) utiliza CancellationToken para comunicar tres eventos principales: ApplicationStarted , ApplicationStopping y ApplicationStopped :

 namespace Microsoft.Extensions.Hosting { /// <summary> /// Allows consumers to be notified of application lifetime events. /// This interface is not intended to be user-replaceable. /// </summary> public interface IHostApplicationLifetime { /// <summary> /// Triggered when the application host has fully started. /// </summary> CancellationToken ApplicationStarted { get; } /// <summary> /// Triggered when the application host is starting a graceful shutdown. /// Shutdown will block until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopping { get; } /// <summary> /// Triggered when the application host has completed a graceful shutdown. /// The application will not exit until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopped { get; } /// <summary> /// Requests termination of the current application. /// </summary> void StopApplication(); } }

A primera vista, puede parecer que los CancellationToken no pertenecen aquí, especialmente porque se usan como eventos. Sin embargo, un examen más detallado revela que estos tokens encajan perfectamente:

  • Son flexibles, lo que permite múltiples formas para que el cliente de la interfaz escuche estos eventos.
  • Son seguros para subprocesos fuera de la caja.
  • Se pueden crear a partir de diferentes fuentes combinando CancellationToken s.

Aunque los CancellationToken s no son perfectos para todas las necesidades de eventos, son ideales para eventos que ocurren solo una vez, como el inicio o la detención de la aplicación.

CancellationToken para el tiempo de espera

De forma predeterminada, ASP.NET nos da muy poco tiempo para cerrar. En aquellos casos en los que queremos un poco más de tiempo, el uso de la clase HostOptions nos permite cambiar este valor de tiempo de espera. Debajo, este valor de tiempo de espera se envuelve en un CancellationToken y se alimenta a los subprocesos subyacentes.

El método IHostedService de StopAsync es un gran ejemplo de este uso:

 namespace Microsoft.Extensions.Hosting { /// <summary> /// Defines methods for objects that are managed by the host. /// </summary> public interface IHostedService { /// <summary> /// Triggered when the application host is ready to start the service. /// </summary> /// <param name="cancellationToken">Indicates that the start /// process has been aborted.</param> Task StartAsync(CancellationToken cancellationToken); /// <summary> /// Triggered when the application host is performing a graceful shutdown. /// </summary> /// <param name="cancellationToken">Indicates that the shutdown /// process should no longer be graceful.</param> Task StopAsync(CancellationToken cancellationToken); } }

Como es evidente en la definición de la interfaz IHostedService , el método StopAsync toma un parámetro CancellationToken . El comentario asociado con ese parámetro comunica claramente que la intención inicial de Microsoft para CancellationToken era un mecanismo de tiempo de espera en lugar de un proceso de cancelación.

En mi opinión, si esta interfaz hubiera existido antes de la existencia de CancellationToken , podría haber sido un parámetro TimeSpan , para indicar cuánto tiempo se permitió procesar la operación de detención. Según mi experiencia, los escenarios de tiempo de espera casi siempre se pueden convertir en un token de CancellationToken con una gran utilidad adicional.

Por el momento, olvidemos que sabemos cómo está diseñado el método StopAsync y, en su lugar, pensemos en cómo diseñaríamos el contrato de este método. Primero definamos los requisitos:

  • El método StopAsync debe intentar detener el servicio.
  • El método StopAsync debe tener un estado de parada elegante.
  • Independientemente de si se logra un estado de detención correcto, un servicio alojado debe tener un tiempo máximo para detenerse, según lo define nuestro parámetro de tiempo de espera.

Al tener un método StopAsync en cualquier forma, cumplimos el primer requisito. Los requisitos restantes son complicados. CancellationToken satisface exactamente estos requisitos mediante el uso de una herramienta de comunicación estándar basada en banderas de .NET para potenciar la conversación.

CancellationToken como mecanismo de notificación

El mayor secreto detrás de CancellationToken es que es solo una bandera. Ilustremos cómo se puede usar CancellationToken para iniciar procesos en lugar de detenerlos.

Considera lo siguiente:

  1. Cree una clase RandomWorker .
  2. RandomWorker debe tener un método DoWorkAsync que ejecute algún trabajo aleatorio.
  3. El método DoWorkAsync debe permitir que la persona que llama especifique cuándo debe comenzar el trabajo.
 public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync() { for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }

La clase anterior satisface los dos primeros requisitos, dejándonos con el tercero. Hay varias interfaces alternativas que podríamos usar para activar a nuestro trabajador, como un lapso de tiempo o una bandera simple:

 # With a time span Task DoWorkAsync(TimeSpan startAfter); # Or a simple flag bool ShouldStart { get; set; } Task DoWorkAsync();

Estos dos enfoques están bien, pero nada es tan elegante como usar un CancellationToken :

 public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync(CancellationToken startToken) { startToken.WaitHandle.WaitOne(); for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }

Este código de cliente de muestra ilustra el poder de este diseño:

 using System; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace CancelToStart { public class Program { static void Main(string[] args) { CancellationTokenSource startCts = new CancellationTokenSource(); startCts.CancelAfter(TimeSpan.FromSeconds(10)); var tasks = Enumerable.Range(0, 10) .Select(i => new RandomWorker(i)) .Select(worker => worker.DoWorkAsync(startCts.Token)) .ToArray(); Task.WaitAll(tasks, CancellationToken.None); } } }

CancellationTokenSource creará nuestro CancellationToken entre bastidores y coordinará la activación de todos los procesos asociados. En este caso, el proceso asociado es nuestro RandomWorker , que está esperando para iniciarse. Este enfoque nos permite aprovechar la seguridad de subprocesos integrada en la implementación predeterminada de CancellationToken .

Una caja de herramientas de token de cancelación expansiva

Estos ejemplos demuestran cómo CancellationToken proporciona una caja de herramientas de soluciones que son útiles fuera de su caso de uso previsto. Las herramientas pueden ser útiles en muchos escenarios que involucran comunicación basada en banderas entre procesos. Ya sea que nos enfrentemos a tiempos de espera, notificaciones o eventos únicos, podemos recurrir a esta elegante implementación probada por Microsoft.

De arriba a abajo, aparecen las palabras "Gold" (color dorado), "Microsoft" y "Partner" (ambas en negro) seguidas del logotipo de Microsoft.
Como Microsoft Gold Partner, Toptal es su red élite de expertos de Microsoft. Cree equipos de alto rendimiento con los expertos que necesita, ¡en cualquier lugar y exactamente cuando los necesite!

Lecturas adicionales en el blog de ingeniería de Toptal:

  • .NET Core: Volviéndose salvaje y de código abierto. Microsoft, ¿por qué te tomó tanto tiempo?
  • Creación de una API web de ASP.NET con ASP.NET Core
  • Cómo Arrancar y Crear Proyectos .NET