Asincronía en .NET 4.5: Task Parallel Library
- Introducción
- Async/Await
- Task Parallel Library
- Introducción a Dataflow
Task Parallel Library
En este artículo voy a describir lo que es la “Task Parallel Library” – la librería de las tareas paralelas – y de qué partes consiste. Para empezar, me gustaría compartir la siguiente cita:
“A partir de .NET Framework 4, la TPL es el modo preferido de escribir código paralelo y multiproceso.” (https://msdn.microsoft.com/es-es/library/dd460717.aspx)
La “Task Parallel Library” ya existía en .NET 4.0, pero el lanzamiento de .NET 4.5 trae mejoras.
Tasks
Las tareas son lo que indica el nombre: pequeñas unidades de trabajo que se pueden ejecutar de una manera asíncrona.
Antes existía – y por supuesto, todavía existe – la posibilidad de usar hilos y el ThreadPool, pero ambas cosas tienen sus desventajas:
Crear tu proprio hilo involucra mucho trabajo – sobre todo si quieres reutilizarlo para distintos trabajos. (Usar un hilo dedicado para cada tarea no es recomendado, por el impacto negativo que tiene el uso excesivo de hilos en el rendimiento de la aplicación.)
Comparado con hilos “puros”, el ThreadPool nos facilita la vida bastante: como indica el nombre, es un gestor de hilos. Acepta tareas y decide donde y cuando ejecutarlas. También maneja automáticamente la creación / destrucción de hilos, para mejorar el rendimiento.
La desventaja del ThreadPool es la falta de control: Una vez que le hemos pasado una tarea…
- …no sabemos cuando o donde se ejecuta, ni lo podemos controlar.
- …no sabemos cuando ha terminado de ejecutarse. (salvo si implementamos esa lógica a mano)
- …no podemos cancelar la tarea.
Task Scheduler
En la “Task Parallel Library” el Task Scheduler decide dónde y cuándo se ejecutan las tareas. Es posible implementar su propio “Task Scheduler” personalizado, para satisfacer necesidades especiales, pero normalmente la implementación que viene por defecto hace un muy buen trabajo.
La implementación que viene por defecto usa el ThreadPool - ya que es una clase muy útil - pero además nos permite todo el control que antes no teníamos – y encima totalmente integrado en C#, con las nuevas palabras clave await y async. ¡La programación asíncrona nunca ha sido más fácil!
Creación de tareas asíncronas: antes y después
Para ilustrar como de fácil es escribir código asíncrono en .NET 4.5, aquí una comparación del código que hacía falta, sin usar la “Task Parallel Library” y el código de ahora, para conseguir lo mismo.
El escenario es simple: Tenemos tres tareas asíncronas que queremos ejecutar de modo serializado: Solo cuando la primera ha terminado vamos a lanzar la segunda, y solo cuando la segunda ha terminado vamos a lanzar la tercera. Vamos a crear una cadena de tareas.
Aquí nuestras tareas:
protected void Laundry()
{
Thread.Sleep(1000);
ResultList.TryAdd(“Laundry(): done!”);
}
protected void Dishes()
{
Thread.Sleep(1000);
ResultList.TryAdd(“Dishes(): Clean!”);
}
protected void IronClothes()
{
Thread.Sleep(1000);
ResultList.TryAdd(“Iron Clothes(): Your mother would be proud of you!”);
}
Las líneas de “Thread.Sleep(1000)” simulan que las tareas estén haciendo algún trabajo de verdad.
Antes: Trabajando con eventos
Para que nos avisen cuando una tarea ha terminado de ejecutarse, tenemos que implementar un evento. He creado mi propia clase de tareas para tenerlo definido en un solo sitio y para poder reutilizarlo:
Aquí nuestras tareas:
// Representa una tarea.
public class MyTask
{
// Un delegate para el evento.
public delegate void MyTaskFinishedDelegate(MyTask sender);
// El evento que se va a activar cuando la
// tarea haya terminado de ejecutarse.
public event MyTaskFinishedDelegate MyTaskFinished;
// El código de la tarea.
readonly Action _run;
// Solo podemos crear una tarea si tenemos algo que ejecutar.
public MyTask( Action run)
{
_run = run;
}
// La función que vamos a registrar en el ThreadPool. Se
// encarga de ejecutar la tarea y de activar el evento
// después.
public void Run(object state)
{
_run();
if (MyTaskFinished != null)
MyTaskFinished(this);
}
}
Aquí la función principal, que lanza la ejecución de la cadena de tareas: (Por ejemplo, se podría llamar pulsando un botón.)
private bool _hasSyncTaskChainFinished = false;
protected void SyncTaskChain(object param)
{
_hasSyncTaskChainFinished = false;
/// Preparar la tarea de la lavandería.
MyTask laundrytask = new MyTask (Laundry);
laundrytask.MyTaskFinished += laundrytask_MyTaskFinished;
// Ejecutar la tarea de la lavandería.
ThreadPool.QueueUserWorkItem(laundrytask.Run);
// No tenemos manera de saber cuando la última
// tarea ha terminado de ejecutarse. Para eso
// utilizamos una variable "global".
while (!_hasSyncTaskChainFinished)
Thread.Sleep(10);
}
Estamos creando una tarea para la lavandería y registramos la siguiente funciona que se va a ejecutar cuando la tarea termina de ejecutarse. La ponemos en la cola del ThreadPool y como no hay manera de saber cuando la cadena ha terminado, tenemos que usar una variable global y comprobar su valor cada X milisegundos.
void laundrytask_MyTaskFinished(MyTask sender)
{
// Nos quitamos del event handler, porque solo
// vamos a ejecutar este código una vez y no
// queremos causar un memory leak.
sender.MyTaskFinished -= laundrytask_MyTaskFinished;
// Preparar la tarea de lavar los platos.
MyTask dishesTask = new MyTask(Dishes);
dishesTask.MyTaskFinished += dishesTask_MyTaskFinished;
// Ejecutar la tarea de los platos.
ThreadPool.QueueUserWorkItem(dishesTask.Run);
}
Esta funciona, y la siguiente, se parecen bastante a la primera: Preparamos la siguiente tarea, registramos la función que se debería llamar y lanzamos la tarea nueva.
void dishesTask_MyTaskFinished(MyTask sender)
{
// Nos quitamos del event handler, porque solo
// vamos a ejecutar este codigo una vez y no
// queremos causar un memory leak.
sender.MyTaskFinished -= dishesTask_MyTaskFinished;
// Preparar la tarea de planchar la ropa.
MyTask ironingTask = new MyTask (IronClothes);
ironingTask.MyTaskFinished += ironingTask_MyTaskFinished;
// Ejecutar la tarea de planchar la ropa.
ThreadPool.QueueUserWorkItem(ironingTask.Run);
}
La última funciona, que se llama cuando la tercera tarea ha terminado de ejecutarse, es un poco distinta:
void ironingTask_MyTaskFinished(MyTask sender)
{
// Nos quitamos del event handler, porque solo
// vamos a ejecutar este codigo una vez y no
// queremos causar un memory leak.
sender.MyTaskFinished -= ironingTask_MyTaskFinished;
// ¡Hemos terminado de ejecutar la cadena de
// tareas! Avisamos a la funciona principal.
_hasSyncTaskChainFinished = true;
}
Lo único que hacemos aquí es cambiar la variable global para avisar a la función principal de que ya hemos ejecutado toda la cadena y de que puede salir del bucle y terminar de ejecutarse.
Después: Lo que queda en .NET 4.5
No hay mucho que explicar con el código que nos queda si usamos las nuevas características de .NET 4.5:
protected async void SyncTaskChain(object param)
{
await Task.Run(() => Laundry());
await Task.Run(() => Dishes());
await Task.Run(() => IronClothes());
}
Creamos una tarea, le damos la función que hace el trabajo y esperamos a que termine. Así tres veces seguidos. ¡Facilísimo!
Conclusión
Como podemos ver, antes de que existía la “Task Parallel Library” programar de forma asíncrono era bastante complejo: No solo hacía falta escribir mucho más código que ahora, si no, también era mucho más difícil entender el flujo del código. Usando la “Task Parallel Library” y las nuevas características de C# - las palabras clave await y async – podemos escribir código asíncrono como si fuera síncrono.
¡Sin embargo, siempre tenemos que tener en cuanta el coste que tiene usar await y async!
Espero que os sirva de ayuda
Comments
- Anonymous
May 19, 2016
Muchas gracias, Todo esta explicado de manera muy sencilla y mas fácil de entender