Manejo de Estados en Tareas Asíncronas de .NET

En el artículo anterior sobre programación asíncrona en .NET, sentí que faltaba algo importante: el manejo de diferentes escenarios. Por ejemplo, cuendo una Tarea (Task) se completa, quiero que ejecute T1; si es cancelada, que ejecute T2; y si... que ejecute T3. ¿Cómo lograr esto?

El proceso fue frustrante y me tomó dos días comprenderlo. Me había preguntado por qué al invocar el método Cancel de CancellationTokenSource, la propiedad IsCanceled de la Tarea seguía siendo False. Finalmente logré capturar el estado correcto, y el problema estaba en mi implementación. Sin más preámbulos, aquí está el código:

 1 namespace ConsolaTareas
 2 {
 3     class Programa
 4     {
 5         static void Main(string[] args)
 6         {
 7             while (true)
 8             {
 9                 var entrada = Console.ReadLine();
10 
11                 if (entrada.ToLower() == "continuar")
12                 {
13                     fuenteCancelacion = new CancellationTokenSource();
14                     tokenCancelacion = fuenteCancelacion.Token;
15                     var tarea = PuntoEntrada();
16                     Console.WriteLine(tarea.Status);
17                 }
18                 else if(entrada.ToLower() == "cancelar") {
19                     fuenteCancelacion.Cancel();
20                 }
21                 else { break; }
22             }
23         }
24 
25 
26         async static Task RealizarTrabajo(string nombreTrabajador)
27         {
28             int contador = 0;
29 
30             while (contador < 10)
31             {
32                 if (tokenCancelacion.IsCancellationRequested)
33                 {
34                     Console.WriteLine("El usuario canceló la tarea");
35                     tokenCancelacion.ThrowIfCancellationRequested();
36                 }
37 
38                 contador++;
39                 Console.WriteLine("{0} contador {1} @ Id Hilo {2}", nombreTrabajador, contador, System.Threading.Thread.CurrentThread.ManagedThreadId);
40                 await Task.Delay(2000);
41             }            
42         }
43 
44         async static Task ManejarCancelacion(Task tareaAnterior)
45         {
46             //await Task.Delay(2000);
47             Console.WriteLine("Tarea de manejo de cancelación ejecutada");
48         }
49 
50         async static Task ContinuarDespues(Task tareaAnterior)
51         {
52             //await Task.Delay(1000);
53             //Console.WriteLine("Tarea de continuación ejecutada @ Estado de la tarea: {1}", tareaAnterior.Status);
54             Console.WriteLine("Tarea de continuación ejecutada");
55         }
56 
57         async static Task PuntoEntrada()
58         {
59             {
60                 var tareaTrabajador = Task.Factory.StartNew(() => RealizarTrabajo("Trabajador1"));
61                 try
62                 {
63                     await tareaTrabajador.Result;
64                     Console.WriteLine("tareaTrabajador ha finalizado, ¿fue cancelada? {0}", tareaTrabajador.Result.IsCanceled);
65                 }
66                 catch (OperationCanceledException)
67                 {
68                     Console.WriteLine("Excepción OperationCanceledException, ¿la tarea fue cancelada? {0}", tareaTrabajador.Result.IsCanceled);
69                 }
70                 
71                 var c1 = tareaTrabajador.Result.ContinueWith(anterior => ManejarCancelacion(anterior), TaskContinuationOptions.OnlyOnCanceled);
72                 var c2 = tareaTrabajador.Result.ContinueWith(anterior => ContinuarDespues(anterior), TaskContinuationOptions.OnlyOnRanToCompletion);
73             }
74         }
75 
76         static CancellationTokenSource fuenteCancelacion;
77         static CancellationToken tokenCancelacion;
78     }
79 }

Como se observa en el código, C1 utiliza el Result de tareaTrabajador para su ContinueWith. Esto significa que la cacnelación real provocada por el Token ocurre dentro del método RealizarTrabajo, pero tareaTrabajador no es una referencia directa al método RealizarTrabajo, sino a una Tarea creada por la Factoría que ejecuta RealizarTrabajo. Por lo tanto, para que C1 y C2 funcionen correctamente, debemos verificar si el método RealizarTrabajo completó su ejecución o fue cancelado, y RealizarTrabajo es el Result de tareaTrabajador. Con esta corrección, el código funciona como se espera.

Al ingresar "continuar" para iniciar el programa, si se ejecuta hasta el final, el resultado será:

Si se ingresa "cancelar" durante la ejecución para cancelar manualmente, el resultado será:

Gracias a mi perseverancia, finalmente entendí el concepto. En resumen, si se escribe:

1 var c1 = tareaTrabajador.ContinueWith(anterior => ManejarCancelacion(anterior), TaskContinuationOptions.OnlyOnCanceled);
2 var c2 = tareaTrabajador.ContinueWith(anterior => ContinuarDespues(anterior), TaskContinuationOptions.OnlyOnRanToCompletion);

entocnes C1 nunca se ejecutará, porque independientemente de si RealizarTrabajo termina normalmente o es cancelada, la Tarea creada por la Factoría (a la que apunta tareaTrabajador) siempre estará en estado RanToCompletion después de que RealizarTrabajo devuelve el control.

Etiquetas: async Task CancellationToken C# .NET

Publicado el 6-26 18:03