Twitter Facebook Google + RSS Feed

Introducción a Task Parallel Library (TPL)

0
.NET Framework

jueves, 15 de octubre de 2009 a las 15:36hs por Gustavo Cantero (The Wolf)

TaskScheduler (ex TaskManager)

Todas las tareas (objetos Task) son administradas por un objeto llamado TaskScheduler, el cual fue incluido en la beta 1 de .NET Framework 4.0, ya que en el CTP para .NET 3.5 el objeto encargado de realizar esta tarea era el TaskManager (que ya no existe).
TaskScheduler es una clase abstracta, con lo cual podemos heredarla y modificarla para crear nuestros propios “programadores de tareas”. Aunque la mayoría de las veces el TaskScheduler por defecto nos alcanzará para nuestros trabajos, puede ser que necesitemos crear uno que agregue algún tipo de prioridad a las tareas para ejecutarlas, o que queramos ejecutarlas usando LIFO o FIFO, o de alguna otra forma no convencional.
Siempre disponemos de un TaskScheduler (o TaskManager) por defecto, al que podemos acceder con la propiedad estática Default, por ejemplo: TaskScheduler.Default. Si creamos nuestra clase heredada de la abstracta y queremos utilizarla en una tarea, simplemente debemos especificarla en el constructor de la misma. Por ejemplo, supongamos que creamos una clase llamada MiTaskScheduler, la cual debe heredar de TaskScheduler:

public class MiTaskScheduler : TaskScheduler
{
    protected override IEnumerable<Task> GetScheduledTasks()
    {
        //Lógica para devolver la lista de tareas a ejecutar
    }

    protected override void QueueTask(Task task)
    {
        //Lógica para agregar una tarea la lista de tareas a ejecutar
    }

    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        //Lógica para establecer si la tarea pasada por parámetro
        //puede ejecutar de manera sincrónica, en cuyo caso, será ejecutada
    }
}

Y queremos correr la rutina “Rutina” utilizando esta clase, entonces deberíamos hacer lo siguiente para ejecutar el objeto Task:

MiTaskScheduler MiScheduler = new MiTaskScheduler();
Task Tarea = new Task(Rutina);
Tarea.Start(MiScheduler);

También se puede establecer el scheduler a usar en las tareas desde el método ContinueWith, lo que nos permite que una tarea use uno y al finalizar llame a otra tarea que utiliza un scheduler distinto.
El objeto TaskScheduler sabe qué cantidad de tareas se pueden ejecutar concurrentemente, valor que puede consultarse a través de la propiedad MaximumConcurrencyLevel. Este valor también podemos modificarlo en nuestras implementaciones de esta clase, con lo cual, nos permite hacer cosas como devolver siempre 1 en caso de estar en debug o dependiendo de un símbolo del compilador. Esto nos permite debugear de manera más clara, ya que todas las tareas se ejecutarán de forma secuencial, aunque obviamente no nos permite probar posibles problemas de concurrencia.
Como ejemplo podríamos establecer en el campo Conditional compilation symbols del proyecto (en la solapa Build) el valor “SINGLE_THREAD”, entonces podemos sobrescribir la propiedad MaximumConcurrencyLevel de la clase creada en el párrafo anterior:

public class MiTaskScheduler : TaskScheduler
{
    protected override IEnumerable<Task> GetScheduledTasks()
    {
        //Lógica para devolver la lista de tareas a ejecutar
    }

    protected override void QueueTask(Task task)
    {
        //Lógica para agregar una tarea la lista de tareas a ejecutar
    }

    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        //Lógica para establecer si la tarea pasada por parámetro
        //puede ejecutar de manera sincrónica, en cuyo caso, será ejecutada
    }

    public override int MaximumConcurrencyLevel
    {
        get
        {
#if SINGLE_THREAD
            return 1;
#else
            return base.MaximumConcurrencyLevel;
#endif
        }
    }
}

Ahora cada vez que queramos forzar a nuestro código a ejecutarse secuencialmente sólo debemos agregar el símbolo SINGLE_THREAD al compilador, o crear una nueva configuración desde el “Configuration manager” para ejecutarlo con o sin este símbolo de manera más fácil.
Si en algún momento dentro de nuestras tareas necesitamos obtener una referencia al scheduler que se está utilizando como contexto de la misma, podemos obtenerlo usando la propiedad estática Current de la clase TaskScheduler.
Por último voy a comentar una novedad que trae esta clase y que no está disponible en el CTP: aquellos que ya hayan programado con threads con Windows Forms o WPF habrán sufrido la necesidad de sincronizar los threads que están corriendo algún proceso en paralelo con el thread principal, que es el único que puede acceder a la interfaz gráfica. Esto ahora se puede hacer de una manera mucho más sencilla, ya que el TaskScheduler posee un método estático llamado FromCurrentSynchronizationContext que devuelve un objeto del mismo tipo, con el cual podemos ejecutar nuestra tarea en el thread que puede acceder a los controles gráficos. Por ejemplo, si necesitamos realizar una tarea en background y que cuando termine actualice la pantalla, podemos hacer algo así:

//Creo la tarea a correr de fondo
Task Tarea = new Task(RutinaBackground);

//Creo la tarea que actualizará la pantalla
Task TareaResultado = Tarea.ContinueWith(
    MostrarResultado,
    TaskContinuationOptions.OnlyOnRanToCompletion,
    TaskScheduler.FromCurrentSynchronizationContext());

//Inicio la tarea
Tarea.Start();

TaskFactory

Esta clase nos permite crear, ejecutar, asignar un TaskScheduler, establecer una cadena de Task y realizar otras tareas de forma más sencilla. Por ejemplo, como comenté antes, en la beta 1 de .NET 4.0 luego de crearse las tareas debe invocarse el método Start para que se agreguen a la lista de rutinas a ejecutar, pero si queremos hacer que apenas se cree ya esté lista para correr (como sucede con el CTP) podemos usar el método StartNew de este objeto, como se muestra a continuación:

Task Tarea = Task.Factory.StartNew(Rutina);

Otro ejemplo podría ser que al finalizar varias tareas se actualice en la pantalla el resultado (algo parecido a lo mostrado anteriormente), entonces podríamos hacer algo así:

//Creo las tareas a correr de fondo
Task[] tareas = new Task[] {
    new Task(Rutina1),
    new Task(Rutina2),
    new Task(Rutina3)};

//Creo la tarea que actualizará la pantalla
Task.Factory.ContinueWhenAll(
    tareas,
    MostrarResultado,
    TaskContinuationOptions.OnlyOnRanToCompletion,
    TaskScheduler.FromCurrentSynchronizationContext());

//Inicio las tareas
tareas[0].Start();
tareas[1].Start();
tareas[2].Start();

Conclusión

Esta librería posee varias otras novedades y los métodos aquí nombrados tienen varias otras sobrecargas, pero espero que esta introducción a la utilización de la Task Parallel Library les haya sido de utilidad y los haya incentivado a utilizarla.
Sólo cabe mencionar que les dejo el código de los ejemplos realizados con Visual Studio® 2008 y con la beta 1 de Visual Studio® 2010. El primero de éstos tiene como requerimiento que se tenga instalada la CTP de la librería.

Proyecto de ejemplo en Visual Studio 2010 beta 1
Proyecto de ejemplo en Visual Studio 2010 beta 1
Proyecto de ejemplo en Visual Studio 2008
Proyecto de ejemplo en Visual Studio 2008

0 comentarios »

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Buscar