TPL – Task

Como comentamos en el anterior artículo las Task son las unidades básicas de ejecución dentro de TPL (Task Parallel Library) y en este doble artículo vamos a ver cuáles son las posibilidades que tenemos para trabajar con las Task dentro de nuestro código.

Una de las misiones de la TPL es ofrece una API consistente para el trabajo concurrente de software, es decir para tareas que se van a ejecutar de manera concurrente en un sistema con más de un procesador. Como bien es sabido la unidad mínima que el SO es cada de enviar para ejecutar es un Thread, pero nosotros aquí estamos hablando de Task. ¿Qué relación hay entre un Thread y un Task?.

Un thread es una unidad mínima y demasiado concreta para ejecutar código de manera concurrente. Dentro de .NET se puede crear una instancia de la clase Thread para ejecutar un método que nosotros queramos dentro de un thread diferente, pero una vez que ese método se ejecuta el Thread termina y se liberan los recursos utilizados por él. Es por eso que utilizar una estructura un poco más de alto nivel nos ayuda a abstraernos de cómo nuestras Task se ejecutan.

Ahora bien esto no quiere decir que las Task *no* se e ejecutan dentro de un thread, sino que también se ha creado una estructura intermedia llamada TaskScheduler que nos permite definir cómo se van a ejecutar nuestras Task. La igualdad con respecto al par Schedule/Thread es muy parecida pues tenemos conceptos muy similares, pero como veremos más adelante las Task son mucho más flexibles que un Thread y permite un sinfín de configuraciones, es más permiten que se ejecuten de manera síncrona, cosa que un thread no puede.

Basic – Creación de una Task.

Para crear una instancia de la clase Task podemos hacerla de varias maneras, aquí tenemos algunos ejemplos.

Task t = new Task(() =>
{
    Console.WriteLine("hola desde un task");
    Thread.Sleep(1000 * 4);
});

Task argumento = new Task(index =>
{
    int value = (int)index;
    for (int i = 0; i < value; i++)
    {
        DoStuff();
    }
    Thread.Sleep(1000 * 4);
}, 90);

En ambos ejemplo se han utilizado Lambdas para crear los delegados que ejecutarán el código pero se puede utilizar el delegado Action y Action<T> para sacar ese valor a un método externo.

Con esto simplemente lo que hemos hecho es definir únicamente el objeto Task y ahora mismo esta simplemente creado pero no se le ha especificado que se tiene que ejecutar. Para ejecutarlo tenemos dos opciones, de manera síncrona y de manera asíncrona (concurrente). Puede parecer algo raro el tener el soporte de ejecución síncrona el algo que está pensado para ejecutarse de manera concurrente siempre, pero es que en algunos casos es útil y así podemos definir todo nuestro trabajo con Task y luego decidir cómo queremos ejecutar.

t.RunSynchronously();            

t.Start();    

En el método Start tenemos una sobrecarga que nos permite especificar cuál es el TaskScheduler en el que queremos ejecutar nuestra Task. Si no especificamos ninguno el sistema automáticamente utiliza un TaskScheduler que utiliza internamente el ThreadPool de Windows para ejecutar el código. Por supuesto podemos definir y crear nuestros propios TaskScheduler simplemente heredando de la clase TaskScheduler. Además del que está definido con el ThreadPool si estamos dentro de una aplicación de UI como Windows Forms o WPF estos disponen de un TaskScheduler propio para ejecutar tareas dentro del bucle de mensajes de la aplicación.

Para poder acceder a este TaskScheduler tenemos que llamar a este código:

TaskScheduler ui = TaskScheduler.FromCurrentSynchronizationContext();

Además de estas opciones tenemos algunas otras con las que podemos trabajar con Task. Podemos esperar la ejecución de una Task concreta un tiempo determinado o simplemente podemos esperar infinitamente hasta que la tarea termine.

Una de las sobrecargas del método wait acepta un objeto de tipo CancellationToken que como su nombre indica es un objeto que nos ayuda a tener un soporte de cancelación de Task que veremos más adelante.

También disponemos de varias propiedades dentro de la clase Task que nos dan información del estado de la tarea como:

  • State: enumerado que nos indica en qué estado está la Task, como: Created, WaitToRun, Running, RanToCompletion, ect.
  • AsynState: objeto de usuario.
  • Exception: si se ha producido una excepción durante la ejecución de la Task y no se ha controlado el estado de la Task será Faulted y en esta propiedad aparecerá un objeto de tipo AggregateException que contiene una lista de todas las excepciones que se han producido durante la ejecución de la Task.
  • Result: obtiene de manera segura el resultado de la ejecución de la Task.

Como última opción veremos que tenemos un método llamado ContinueWith que nos permite ejecutar la Task especificada justo después de que esta termine pudiendo así enlazar Task y crear dependencias entre ellas. Esto es muy útil cuando se trabaja con funciones que van a tardar mucho en ejecutarse como una lectura de un fichero, una query en una base de datos o una actividad en background.

Con lo que dijimos antes vamos a ver un ejemplo utilizando TaskScheduler.FromCurrentSynchronizationContext() en una aplicación WPF para ver ContinuwWith.

Como bien es sabido en WPF y en Windows Forms no es posible actualizar el estado de un objeto de UI desde el Thread que no es el Thread que creo el objeto, es decir si estamos ejecutando código desde otro Thread, algo normal con las Task, no vamos a poder actualizar el resultado de nuestra Task, así que tenemos que sincronizar el acceso.

Task.Factory.StartNew(() =>
{
    // simulando una operacion lenta

    Thread.Sleep(1000 * 2);
    return "hola";

}).ContinueWith(task =>
{
    result.Text = task.Result;

}, TaskScheduler.FromCurrentSynchronizationContext());

Aquí está el disponible el código de ejemplo.

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

 

Saludos. Luis.

3 comentarios sobre “TPL – Task”

  1. A mi para lo que me gusta el ContinueWith es para implementar el viejo patrón pipe, el que podemos paralelizar ciertas partes pero necesitamos el resultado total de esa paralelización para continuar con el siguiente paso del proceso.

    Excelente serie de artículos.

    Un saludo titán.

Deja un comentario

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