Modelos de ejecución asíncrona y Windows Phone

  • Por
  • .NET
Breve introducción a los diferentes modelos de ejecución asíncrona que tenemos en .Net y cómo podemos utilizar algunos de los nuevos que no vienen de serie en Windows Phone 7.
La programación asíncrona se ha convertido en una necesidad para evitar el bloqueo del interfaz de usuario cuando ejecutamos tareas más largas de unos pocos milisegundos: acceso a recursos remotos, lectura de ficheros en disco, cálculos muy largos, etc. Cualquier bloqueo del UI, aunque sea de medio segundo, dará la sensación de una baja calidad de la aplicación. Por este motivo se han ido desarrollando mecanismos que nos faciliten la tarea de manejar diferentes hilos de ejecución.

En este artículo encontraréis una breve introducción a los diferentes modelos de ejecución asíncrona que tenemos en .Net y cómo podemos utilizar algunos de los nuevos que no vienen de serie en Windows Phone 7, como ocurre con TPL.

Un poco de historia (subjetiva)

Antes de que existiera la programación multihilos y mucho antes de los procesadores multinúcleo, los usuarios de casi cualquier aplicación teníamos que esperar una eternidad a que se realizaran ciertas operaciones. Algunos aprovechaban para hacer otras cosas como tomarse un café, ir al baño, darse un paseo por el bosque, otros nos entreteníamos con el ruidito que hacía la cinta del Spectrum mientras cargaba un programa juego. Eran otros tiempos y los pocos usuarios que éramos nos tomábamos la vida con filosofía y muchos cafés.

Poco a poco los usuarios se volvieron cada vez más impacientes exigentes y dejaron de admitir aquellas agradables pausas que les ofrecíamos los programadores para que pudieran relajarse y relacionarse con sus compañeros. Los fabricantes de sistemas operativos crearon la multitarea: podíamos utilizar varias aplicaciones simultáneamente, así que los usuarios vieron la luz y se pusieron muy pesados con el resto de desarrolladores, los pobres mortales que escribían aplicaciones de gestión, exigiendo que las aplicaciones no se quedaran paradas mientras realizaban alguna complicada operación que les parecía que tardaba demasiado.

Por mucho que optimicemos existen multitud de esperas que no dependen de nosotros. A algunos se les ocurrió que valía más parecer rápido que serlo y vieron que la solución era utilizar el tiempo que el procesador estaba ocioso para realizar las tareas largas, procurando no influir demasiado sobre el rendimiento del interfaz de usuario.

Patrones multihilo

Todo esto nos llevó hasta nuestro viejo compañero el subproceso (o hilo), que permitía hacer todo esto y mucho más, utilizando técnicas muy complejas como los bloqueos, semáforos y mensajes para poder sincronizar los diferentes hilos.

La programación con múltiples hilos puede llegar a ser muy complicada y difícil de entender, pues al estar acostumbrados a un lenguaje imperativo para programar (como en nuestro caso C#) tendemos a pensar en la ejecución de manera secuencial. Cuando utilizamos múltiples hilos rompemos esa secuencia en tareas que pueden ejecutarse en paralelo.

En .Net podemos hacer uso de la clase Thread que encapsula la funcionalidad de los hilos del sistema operativo, pero para facilitarnos las tareas más comunes surgieron otros dos patrones de desarrollo de métodos asíncronos. Estos dos patrones nos facilitaron mucho la tediosa tarea de ir creando, manteniendo y sincronizando diferentes hilos de ejecución:

  • APM o Asynchronous Programming Model: gracias al modelo de programación asíncrona pudimos ahorrarnos escribir el mismo código una y otra vez para crear un hilo en segundo plano que realizara esas tareas. El modelo consiste en una pareja de métodos Begin y End: en Begin pasaremos un método "callback" que será llamado al acabar la ejecución y utilizaremos End para recuperar la información que ha procesado el método asíncrono.
  • EAP o Event-based Asynchronous Pattern: aquí tenemos un método OperationAsync para lanzar la ejecución asíncrona y un evento OperationCompleted donde recibiremos los datos. En Silverlight lo conocemos bien pues lo utilizamos en multitud de clases, como por ejemplo WebClient.DownloadStringAsync y WebClient.DownloadStringCompleted
Las clases que implementan EAP las podemos usar creando manejadores de eventos explícitos, tal como usamos los eventos de pulsación de un botón. También podemos utilizar métodos anónimos para facilitar un poco su lectura.

Utilizando un manejador de evento explícito podemos tratar el resultado del método asíncrono cuando este acaba su ejecución de la siguiente manera:

public void DescargarDatos()
{
var webClient = new WebClient();
webClient.DownloadStringCompleted += webClientDownloadStringCompleted;
webClient.DownloadStringAsync(new Uri("http://jmservera.wordpress.com/feed/"));
this.contenedor.Text="DescargaComenzada";
}

private void webClientDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
this.contenedor.Text=e.Result;
}

Cuando usamos este método en Silverlight el sistema volverá a sincronizar el hilo de UI en el evento Completed para que no tengamos que utilizar explícitamente el Dispatcher. Puede que alguna implementación de llamada asíncrona EAP no tenga en cuenta esta característica de la programación en el UI.

Con este modelo tenemos el manejador del resultado algo lejos de la llamada y en códigos muy largos podemos perder un poco la perspectiva de lo que estamos haciendo.

Gracias a los métodos anónimos podemos escribir directamente dentro del mismo método, lo que nos facilitará un poco la legibilidad de nuestro código:

public void DescargarDatos()
{
var webClient = new WebClient();
webClient.DownloadStringCompleted += (o,e) =>
{
this.contenedor.Text=e.Result;
};
webClient.DownloadStringAsync(new Uri("http://jmservera.wordpress.com/feed/"));
this.contenedor.Text="DescargaComenzada";
}

Aun así la secuencia natural no se ve reflejada en el código, pues la acción que pretendemos realizar tras la descarga la tenemos que indicar antes de llamar a la ejecución de la misma. Nos queda el código desordenado.

Task Parallel Library

TPL es una librería pensada para paralelizar tareas de manera sencilla y así mejorar el rendimiento de nuestra aplicación. Aunque su foco es otro, también nos viene muy bien para crear tareas asíncronas y encadenarlas en el orden en el que se van a ejecutar.

public void DescargarDatosTPL()
{
var syncContext= TaskScheduler.FromCurrentSynchronizationContext();
var webClient = new WebClient();
//ejecutamos la tarea asincrona
webClient.DownloadStringTaskAsync(new Uri("http://jmservera.wordpress.com/feed/"))
.ContinueWith((s) =>
{
//y continuamos tras finalizar la descarga
contenido.Text = s.Result;
}, syncContext);

// aquí continuamos en el hilo principal mientras se ejecuta la tarea
// de descarga
contenido.Text = "Descargando...";
}

Como podemos ver en el código las llamadas se encadenan en tareas en la secuencia natural, primero descargamos y luego utilizamos el resultado.

Para poder asignar el texto al contenedor hemos utilizado un TaskScheduler que sincronizará automáticamente el hilo de ejecución con el contexto que nosotros le indicamos, no nos tenemos que preocupar de llamar al Dispatcher, si hace falta el TaskScheduler se encargará de ello por nosotros.

TPL para Windows Phone 7

Para poder usar TPL en WP7 necesitaremos recurrir a la versión que hay disponible para descarga a través de NuGet. Dado que no es la versión oficial, no tendremos el método DownloadStringTaskAsync en el WebClient. TPL tiene un wrapper para poder convertir métodos APM en TPL. Para los EAP no existe uno, pero podemos crearlo utilizando la clase TaskCompletionSource. Para nuestro ejemplo crearemos un método de extensión y así se parecerá más a los nuevos métodos que están por venir; habrá que ir con cuidado de liberar los manejadores de evento:

public static class WebClientExtension
{
public static Task<string> DownloadStringTaskAsync(this WebClient webClient, Uri uri)
{
TaskCompletionSource<string> tcs =
new TaskCompletionSource<string>();

DownloadStringCompletedEventHandler completedHandler = null;
completedHandler=(o, e) =>
{
if (e.Cancelled)
{
tcs.TrySetCanceled();
}
else if (e.Error != null)
{
tcs.TrySetException(e.Error);
}
else
{
tcs.TrySetResult(e.Result);
}
//hay que limpiar referencias para
//evitar problemas de memoria
webClient.DownloadStringCompleted -= completedHandler;
};

webClient.DownloadStringCompleted += completedHandler;
try
{
webClient.DownloadStringAsync(uri);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
webClient.DownloadStringCompleted -= completedHandler;
}
return tcs.Task;
}
}

Una vez creada la tarea de esta manera podremos llamar al método DownloadStringTaskAsync como hicimos en el ejemplo anterior.

Lanza y Espera con Async y Await

Como hemos visto poco a poco vamos mejorando la legibilidad del código, de manera que nuestro código se parezca cada vez más al código que escribiríamos si realizáramos la operación de manera síncrona. La última vuelta de tuerca ha sido la creación de las nuevas construcciones del lenguaje async y await.

Durante el Mix 2011 se anunció la Community Technology Preview de Async, una nueva característica del lenguaje C# para la creación de funciones asíncronas, que está madurando y, como se vio en el Build Windows, ya forma parte del framework 4.5. La nueva manera de escribir nuestro código será ésta:

async void DescargarDatosAsync()
{
var webClient = new WebClient();
var data = await webClient.DownloadStringTaskAsync(
new Uri("http://jmservera.wordpress.com/feed/"));
this.contenedor.Text = data;
}

El truco está en la palabra reservada await que le indica al compilador que llamamos a un método asíncrono y que debe continuar la ejecución hasta que encuentre código que solicita los datos esperados del método asíncrono. Para ello el compilador creará una máquina de estados que generará los métodos necesarios para manejar los callbacks que sean necesarios.

Para Windows Phone 7.1 la última versión de la Async CTP no funciona, así que para probarla necesitaréis el Visual Studio 11 Developer Preview.

Cuando llamemos al método DescargarDatosAsync nuestro código no se parará ahí, continuará la ejecución y quien se encarga de todo esto es el propio compilador de C#.

¿Sabes cuál fue el primer sistema operativo doméstico con multitarea preemtiva? Descúbrelo en los enlaces de este artículo.

¿Qué necesito?