TPL – Cancelación de Task

¡Hola a todos! Seguimos con los post sobre Task Parallel Library de .NET Framework 4, en este artículo vamos a ver cuál es el soporte de cancelación de Task que tenemos en TPL.

Como hemos comentado en el artículo anterior las Task son la unidad mínima de ejecución de TPL, incluso PLINQ (Parallel LINQ) utiliza Task internamente para sus operaciones. También hemos visto como las Task tienen alguna similitud con los Worker Threads del ThreadPool de .NET.

Cuando nosotros en código lanzamos un Worker Thread con el método ThreadPool.QueueWorkItem, estamos encolando un trabajo para que se ejecute en otro Thread diferente, pero no tenemos control en como este Worker thread se ejecuta, de hecho no podemos saber cuándo se ha completado, no podemos cancelarlo y no podemos devolver un resultado de esa ejecución asíncrona.

Esto ahora con las Task cambia, ahora a través de la propiedad Status podemos saber en qué estado está nuestra Task, la propiedad Result nos dará el resultado de la ejecución de la Task. Pero, ¿Qué hay de la cancelación?.

Cancelar un Task no es una tarea trivial, porque implica muchas decisiones y puede conllevar muchos errores. Si pensamos en cómo se cancela un Thread, el CLR lo que hace es inyectar un ThreadAbortException en la ejecución del thread haciendo que este se aborte, pero lo que no podemos controlar es en qué punto de la ejecución del thread se va a producir esto, y lo más peligroso de eso es que podemos estar dentro de un bloque de lock y la inyectarse la excepción puede pasar que nunca se salga de ese bloque de lock, haciendo que pueda pasar un deadlock en nuestro código. Este es solo un ejemplo de lo que puede pasar pero por supuesto puede ser mucho más complicado.

¿Cómo se puede cancelar una Tarea de manera segura?

Hay dos opciones posibles, si la tarea esta creada y se ha llamado al método Start y la Task está esperando para ejecutarse (Status=WaitToRun) y se cancela TPL automáticamente cancelará la tarea por nosotros (Status=Cancelled). Pero si la tarea está ejecutándose o esperando a un evento externo se realiza de otra manera.

Para poder ver esta segunda parte tenemos que comprender primero como se gestiona esto en código para entender cuál es el mecanismo que TPL nos proporciona para cancelar Task.

Task t = new Task((index) =>
{
    Thread.SpinWait(1000);
    if (!cancellationSource.IsCancellationRequested)
    {
        Thread.Sleep(r.Next(0, 1000));
    }
}, i, cancellationSource.Token);
list.Add(t);

Si nos fijamos en este ejemplo de código al crear una instancia de la clase Task ahora pasamos por parámetro un CancellationToken que nos va a permitir comunicarnos con el sistema de cancelación de TPL. Ese CancellationToken viene de una estructura llamada CancellationTokenSource que es realmente la que gestiona la comunicación de la cancelación de las Task.

Lo primero que hacemos es generar una lista con todas las Task que vamos a ejecutar, con eso lo que tenemos es instancias de la clase Task, pero todavía no se están ejecutando, es después cuando llamamos al método Start de todos los objetos Task a la vez.

Como el TaskScheduler predeterminado es el ThreadPool de .NET al principio en una carga de trabajo tan grande 10000 Task, el ThreadPool se tendrá que tunear a sí mismo para empezar a crear Workers Threads y aumentar dinámicamente la capacidad de ejecución por nosotros. Esta es una de las cosas más cómodas de TPL, porque nosotros no tememos que directamente estar pendiente de la heurística del ThreadPool para saber cuál es el número de Threads ideal para nuestro sistema.

Además de todo esto tenemos un método que nos permite cancelar todas las Task a la vez.

public void CancelTask()
{
    cancellationSource.Cancel();
}

Simplemente lo que hacemos es llamar a Cancel del objeto CancellationTokenRequest y este se comunicará con la infraestructura de TPL para empezar la cancelación. Como dijimos antes la cancelación de tareas que están es estado WaitingToRun es automática, pero las que se están ejecutando es más complicado.

Si nos fijamos en el ejemplo de arriba, en el cuerpo del método que se ejecutará por cada Task, hay una sentencia condicional if !cancellationSource.IsCancellationRequested en la que estamos comprobando que no se ha realizado una solicitud de cancelación de tareas. Esto es así porque es la única manera segura y organizada de cancelar un Task. Este ejemplo no es muy representativo para el soporte de cancelación, pero por ejemplo si en esta Task se estuvieran generando por pasos un informe que tarda mucho tiempo, generar el reporte, consulta a la base de datos, ect, podemos hacer comprobaciones en cada paso de que se ha solicitado una cancelación de la Task y así de manera segura cancelar la petición y liberar todos los recursos generados por ese reporte.

Para poder sacar todo el partido de la cancelación en nuestras Task, tenemos que trabajar activamente con TPL para que la experiencia de cancelación sea la más adecuada a cada situación, habrá pasos que no se puedan cancelar hasta un determinado punto, en cualquier caso TPL nunca abortará la ejecución de ese Task por nosotros, pues como he comentado antes esto puede conllevar muchos problemas.

image

Otro de los escenarios posibles cuando se trabaja con cancelación es que de alguna manera nos gustaría saber cuándo un CancellationToken se ha cancelado, para eso tenemos varias opciones, porque el propio CancellationToken tiene un método para que registremos un delegado de tipo Action que será llamado cuando se cancele el CancellationToken. También existe la opción de registrar un objeto de tipo ICancelableOperation, que es la interfaz que implementan el tipo CancellationTokenSource para poder enlazar así objetos de cancelación y crear dependencias entre ellos.

Aquí os podéis descargar el código de ejemplo de la aplicación de WPF.

http://www.luisguerrero.net/downloads/TaskCancel.zip

Saludos. Luis.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *