Twitter Facebook RSS Feed

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

Tareas (Tasks)

Cuando utilizamos alguno de los métodos antes descriptos la ejecución de la aplicación queda detenida en esa línea hasta que se terminen de correr todas las tareas pendientes. Otra de las posibilidades que tenemos para ejecutar código en paralelo utilizando TPL es creando objetos Task, los cuales se pueden lanzar y seguir ejecutando el código de la aplicación.
La clase Task, del namespace System.Threading.Tasks, posee varios constructores, pero el más sencillo tiene como parámetro un delegado que debe apuntar a la tarea a ejecutar en paralelo. Luego de creada, cuando queremos que comience a correr, necesitamos ejecutar el método Start, el cual le indicará que, cuando disponga de un procesador libre, inicie la ejecución de la tarea.
Si usamos el CTP hay algunas diferencias en la creación y utilización de esta clase en comparación con la beta 1 de .NET 4.0, por ejemplo, para crearlo el lugar de utilizar el constructor necesitamos usar el método estático Create, el cual admite los mismos parámetros que mencioné antes. Una vez creado este objeto la tarea estará disponible para ejecutarse en paralelo apenas disponga de un procesador libre donde correr (que puede ser inmediatamente) sin necesidad de llamar al método Start (el cual aquí no existe).
Si necesitamos asegurarnos de que la tarea se haya ejecutado en algún punto de nuestro código simplemente debemos utilizar el método Wait, el cual esperará hasta que termine su ejecución. Si necesitamos esperar como máximo durante un tiempo determinado, este método tiene dos sobrecargas, las cuales poseen parámetros para suministrarle un TimeSpan o un entero para establecer el tiempo o la cantidad de milisegundos de espera máxima respectivamente. En caso de estar corriendo la aplicación en una máquina con un solo procesador, al momento de llamar al método Wait se comenzará a ejecutar la tarea, ya que la creación de un nuevo thread, en una máquina donde no hay más procesadores donde alojarlo, genera una disminución en la performance. Cualquier excepción generada en la rutina ejecutada por la tarea es relanzada al ejecutar este método.
También disponemos de los métodos estáticos WaitAll y WaitAny, los cuales poseen las mismas sobrecargas que Wait, pero en lugar de poder pasarle como parámetro un objeto Task se puede pasar un vector de estos objetos, permitiendo esperar hasta que se hayan ejecutado todas éstas tareas (WaitAll) o cualquier de ellas (WaitAny).
Por ejemplo, supongamos que queremos ejecutar tres tareas en paralelo y luego, una vez que hayan finalizado, avisarle al usuario que el proceso ha concluido. Para esto necesitamos crear tres objetos Task que “apunten” a las rutinas a ejecutar y esperar que las todas finalicen. Un ejemplo de eso es el siguiente código:

//Creo los objetos Task
Task[] tareas = new Task[] {
    new Task(Rutina1),
    new Task(Rutina2),
    new Task(Rutina3)};

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

//Espero que se ejecuten
Task.WaitAll(tareas);

MessageBox.Show("¡Listo!");

Como comenté antes, el constructor de esta clase posee varias sobrecargas, pero casi todas tienen un parámetro para asignar el valor que se le pasará a la rutina a invocar. En el CTP del TPL (no así en la beta 1 del .NET Framework 4.0) este parámetro no es opcional, por lo tanto las rutinas deben poseer, aunque no se use, un parámetro de entrada.
En caso de que queramos cancelar una tarea poseemos los métodos Cancel y CancelWait. Lo que ambos hacen es cambiar el valor de la propiedad booleana de sólo lectura IsCancellationRequested a “verdadero” (excepto en el CTP que usa la propiedad IsCanceled). Esto no significa que la invocación de éstos métodos detengan la ejecución de la tarea, sino que somos nosotros, dentro de la rutina a ejecutarse en el Task, los encargados de verificar periódicamente el valor de esta propiedad para que, en caso de modificarse, detener la ejecución. Para obtener el valor de esta propiedad desde la rutina podemos utilizar la propiedad estática Current de la clase Task, la cual nos devuelve, en caso de estar corriendo en el thread de una tarea, una referencia a la misma, desde la cual podemos obtener el valor de la propiedad en cuestión.
El método CancelWait se diferencia de Cancel en que éste espera hasta que la tarea haya sido cancelada y haya dejado de correr para continuar con el hilo de ejecución. Al igual que el método Wait, posee dos sobrecargas para establecer el tiempo máximo de espera.
Otra propiedad interesante de la clase Task de la cual podemos hacer uso es IsCompleted, cuyo valor es verdadero sólo en caso de que la tarea ya haya finalizado, o falso en caso contrario.
En la beta de .NET Framework 4.0 agregaron una nueva propiedad (no disponible en el CTP para .NET 3.5) llamada IsFaulted, la cual vale verdadero sólo si la tarea fue finalizada por una excepción no controlada.
Si necesitáramos anidar varias rutinas en una tarea, o sea, que una tarea se ejecute luego de finalizada otra, podemos utilizar el método ContinueWith, al cual el podemos establecer distintas rutinas a ejecutarse luego de finalizada la actual o sólo según su estado de finalización. Por ejemplo, si queremos que luego de ejecutarse una tarea determinada se ejecute la rutina Fin, sin importar su estado al finalizar, deberíamos ejecutar el siguiente código:

Tarea.ContinueWith(Fin);

Pero para hacerlo más interesante vamos a pensar en un ejemplo un poco más complejo: supongamos que necesitamos hacer que se ejecute la tarea, si ésta finalizó correctamente que ejecute el método Finalizar, pero si produjo una excepción que antes ejecute CorrecionError y luego el método Finalizar. El gráfico siguiente muestra lo que queremos hacer de forma visual:

Diagrama de ejemplo de Task.ContinueWith

Para hacer lo anteriormente expuesto necesitamos utilizar una sobrecarga del método ContinueWith, la cual necesita como segundo parámetro un valor del enumerador TaskContinuationOptions, el que nos permite establecer bajo que condición de finalización de la tarea queremos ejecutar la siguiente. Este enumerador posee varios posibles valores, por ejemplo, correr sólo cuando se haya cancelado la tarea anterior (OnlyOnCanceled), sólo cuando no haya lanzado excepciones (NotOnFaulted) o cuando si las haya lanzado (OnlyOnFaulted), cuando haya finalizado correctamente (OnlyOnRanToCompletion), y varias opciones más.
En la versión del CTP este enumerador no existe, en su lugar se utiliza el TaskContinuationKind, el cual dispone de algunas opciones como OnCompletedSuccessfully, que ejecutará la tarea sólo en caso de que la actual finalice satisfactoriamente, OnFailed la ejecutará solamente si lanzó una excepción, OnAborted para que se ejecute cuando se haya cancelado la tarea, u OnAny, donde se ejecutará al finalizar la tarea actual sin importar su estado o la causa por la cual terminó su ejecución.
Entonces, para pasar a código de .NET 4.0 lo que expuse en el gráfico, deberíamos escribir lo siguiente:

//Creo la tarea para que utilice el método "Rutina"
Task Tarea = new Task(Rutina);

//Especifico que rutina ejecutar en caso de generarse una excepción
Task TareaConError = Tarea.ContinueWith(CorreccionError, TaskContinuationOptions.OnlyOnFaulted);

//Especifico que rutina ejecutar al finalizar la tarea
Task TareaFinalizar = Tarea.ContinueWith(Finalizar, TaskContinuationOptions.OnlyOnRanToCompletion);
Task TareaFinalizar2 = TareaConError.ContinueWith(Finalizar);

//Inicio la tarea
Tarea.Start();

Pero si quisiéramos ejecutarlo con el CTP debería ser algo como esto:

//Creo la tarea para que utilice el método "Rutina"
Task Tarea = Task.Create(Rutina);

//Especifico que rutina ejecutar en caso de generarse una excepción
Task TareaConError = Tarea.ContinueWith(CorreccionError, TaskContinuationKind.OnFailed);

//Especifico que rutina ejecutar al finalizar la tarea
Task TareaFinalizar = Tarea.ContinueWith(Finalizar, TaskContinuationKind.OnCompletedSuccessfully);
Task TareaFinalizar2 = TareaConError.ContinueWith(Finalizar);

Hay que tener en cuenta que a las rutinas llamadas utilizando el método ContinueWith no se les puede pasar valores como parámetros, en cambio debe tener como parámetro un objeto del tipo Task, donde se pasará la instancia de la tarea previa. Por ejemplo, la rutina CorreccionError debería quedar parecida a esto:

private void CorreccionError(Task TareaPrevia)
{
    Exception ex = TareaAnterior.Exception;
    //Acá hago algo dependiendo de la excepción
}

0 comentarios »

Deja un comentario

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