Un CancellationToken permite la cancelación cooperativa entre subprocesos, elementos de trabajo del grupo de subprocesos u objetos Task . En este artículo, me gustaría discutir el mecanismo que se aplica a los objetos Task.
Cuando ejecuta una tarea en C#, puede llevar un tiempo ejecutarla. En algunos casos, le gustaría cancelar una operación tan larga. Puede haber varias razones: tiempo de espera de la operación, exceder los límites de recursos, etc.
CancellationTokenSource
que indique la cancelación del token.CancellationTokenSource.Token
como un objeto token a la tarea.CancellationTokenSource.Cancel()
que establece la propiedad CancellationToken.IsCancellationRequested
en un valor true
. Eso significa que el método Cancel()
no cancela la operación en sí. Simplemente cambia el valor de la propiedad IsCancellationRequested
. Nosotros, como desarrolladores, tenemos que definir la lógica de cancelación por nosotros mismos.
El tipo CancellationTokenSource
implementa la interfaz IDisposable
y debe liberarse cuando se completa una tarea. Se puede hacer manualmente llamando al método Dispose()
o vi using
la construcción.
Ejemplo de código para demostrar el algoritmo anterior:
// initialize cancellation objects CancellationTokenSource cancelTokenSource = new CancellationTokenSource(); CancellationToken token = cancelTokenSource.Token; // execute a parallel operation Task task = new Task(() => { some_operations }, token); task.Start(); // cancel the operation cancelTokenSource.Cancel(); // release resources cancelTokenSource.Dispose();
Discutamos el paso #3 en detalle. Hay dos formas de definir la lógica de finalización de la tarea mediante un token de cancelación:
return
para salir de la ejecución de la tarea. En este caso, el estado de la tarea será TaskStatus.RunToCompletion
.OperationCanceledException
a través de la llamada al método ThrowIfCancellationRequested()
. En este caso, el estado de la tarea será TaskStatus.Canceled
.return
public static void Main(string[] args) { CancellationTokenSource cancelTokenSource = new CancellationTokenSource(); CancellationToken token = cancelTokenSource.Token; Task task = new Task(() => { for (int i = 1; i < 100; i++) { if (token.IsCancellationRequested) { Console.WriteLine("Operation is canceled"); return; } Console.WriteLine($"Count is equal to '{i}'"); //add some timeout to emulate real-life execution Thread.Sleep(10); } }, token); task.Start(); // add some timeout to emulate real-life execution Thread.Sleep(100); // cancel the parallel operation cancelTokenSource.Cancel(); // wait till the operation is completed task.Wait(); // check the operation status Console.WriteLine($"Task Status is equal to '{ task.Status }'"); // release resources cancelTokenSource.Dispose(); }
El resultado de esta ejecución es el siguiente:
Count is equal to '1' Count is equal to '2' Count is equal to '3' Count is equal to '4' Count is equal to '5' Operation is canceled Task Status is equal to 'RanToCompletion'
ThrowIfCancellationRequested()
public static void Main(string[] args) { CancellationTokenSource cancelTokenSource = new CancellationTokenSource(); CancellationToken token = cancelTokenSource.Token; Task task = new Task(() => { for (int i = 1; i < 100; i++) { if (token.IsCancellationRequested) token.ThrowIfCancellationRequested(); Console.WriteLine($"Count is equal to '{i}'"); //add some timeout to emulate real-life execution Thread.Sleep(10); } }, token); try { task.Start(); // add some timeout to emulate real-life execution Thread.Sleep(100); // cancel the parallel operation cancelTokenSource.Cancel(); // wait till the operation is completed task.Wait(); } catch (AggregateException ae) { foreach (Exception e in ae.InnerExceptions) { if (e is TaskCanceledException) Console.WriteLine("Operation is canceled"); else Console.WriteLine(e.Message); } } finally { // release resources cancelTokenSource.Dispose(); } // check the operation status Console.WriteLine($"Task Status is equal to '{ task.Status }'"); }
El resultado de esta ejecución es el siguiente:
Count is equal to '1' Count is equal to '2' Count is equal to '3' Count is equal to '4' Count is equal to '5' Operation is canceled Task Status is equal to 'Canceled'
La excepción lanzada aparecerá como una InnerException
de AggregateException
. Si la tarea se canceló a través de la llamada al método ThrowIfCancellationRequested()
, la excepción será del tipo TaskCanceledException
. El código verifica este tipo para un manejo adecuado; de lo contrario, maneje otro motivo de excepción.
La excepción se lanzará solo en caso de que se llame al método Wait()
o WaitAll()
para la tarea. De lo contrario, no se lanza ninguna excepción, solo se establece TaskStatus.Canceled
.
Otra forma de definir la lógica de la cancelación de la tarea es usar el método Register() . Registra un delegado de acción que se llamará cuando se cancele CancellationToken .
public static void Main(string[] args) { CancellationTokenSource cancelTokenSource = new CancellationTokenSource(); CancellationToken token = cancelTokenSource.Token; Task task = new Task(() => { int i = 1; token.Register(() => { Console.WriteLine("Operation is canceled"); i = 100; Console.WriteLine($"Count is equal to '{i}'"); }); for (; i < 100; i++) { Console.WriteLine($"Count is equal to '{i}'"); //add some timeout to emulate real-life execution Thread.Sleep(10); } }, token); task.Start(); // add some timeout to emulate real-life execution Thread.Sleep(100); // cancel the parallel operation cancelTokenSource.Cancel(); // wait till the operation is completed task.Wait(); // check the operation status Console.WriteLine($"Task Status is equal to '{ task.Status }'"); // release resources cancelTokenSource.Dispose(); }
El resultado de esta ejecución es el siguiente:
Count is equal to '1' Count is equal to '2' Count is equal to '3' Count is equal to '4' Count is equal to '5' Operation is canceled Count is equal to '100' Task Status is equal to 'RanToCompletion'
En este código, el método cancelTokenSource.Cancel()
se llama y se activa el delegado definido en el método token.Register()
. En este ejemplo, el código establece la variable i
en el valor 100
, lo que provoca el final de la ejecución de la tarea.
Si el código no espera la competencia de la operación, el estado de la tarea será TaskStatus.Running
. Si se llama al método Wait()
o WaitAll()
, el estado de la tarea será TaskStatus.RanToCompletion
.
La cancelación de la tarea es muy importante para optimizar la lógica de su aplicación. Es posible que deba cancelar la tarea por muchas razones: tiempo de espera de la operación, exceso de los límites de recursos, etc. Siempre debe manejar la lógica de cancelación usted mismo. Puede hacerlo mediante el operador de return
o mediante la llamada al método ThrowIfCancellationRequested()
.