Programación asíncrona avanzada con C# 5.0 y Microsoft .NET Framework 4.5 (2/3)

 

NAG 2012.11.07

Presentación

La gestión simplificada de operaciones asíncronas y paralelas ha mejorado sustancialmente (y ha sido incorporada como una característica de base) en la versión 4.5 de .NET Framework (Visual Studio 2012).

Previamente a esta versión ya podíamos trabajar con estos conceptos de tarea, operación asíncrona, etc. (desde la versión 4.0 de .NET Framework y Visual Studio 2010) pero requería la instalación y referencia de una serie de bibliotecas y extensiones adicionales.

Como ya he comentado en el documento anterior de esta serie (Programación asíncrona con C# 5.0) se recomienda el uso del modelo de métodos asíncronos (conocido como Async CTP) y ejecución en paralelo (TPL – Task Parallel Library) incluidos ahora con el entorno de programación y con Visual Studio 2012, aunque no es la única forma de gestionar estas problemáticas.

Por su parte Windows RT ha simplificado las posibilidades que existen para la gestión de hilos de programación, haciendo que tenga más peso aún las funciones de gestión de tareas (Task) que en otras versiones (por ejemplo Windows 8 ‘completo’).

Algunos conceptos de paralelismo

Básicamente podríamos dividir las operaciones paralelas en dos grupos (cada una con sus problemáticas particulares):

·         Gestión de datos en paralelo (paralelismo con datos)

·         Gestión de tareas paralelas

En el primer caso (gestión de datos en paralelo), un mismo método se aplica (potencialmente en paralelo si el hardware y el entorno lo permite) a diferentes elementos de una secuencia de datos. Por ejemplo un método que trata un objeto de datos específico, ejecutado para una lista de objetos, de forma paralela (teóricamente se podría asignar el tratamiento de cada objeto de la lista a una CPU diferente o a un núcleo diferente de una CPU con múltiples núcleos).

Este primer caso está contemplado con los métodos que existen en el espacio de nombres Parallel, por ejemplo Parallel.ForEach (que como su nombre indica trabaja sobre una secuencia de objetos – equivalente a un foreach, pero con potencialidad de hacerlo en paralelo). (Esta característica es también la empleada por PLINQ – Parallel Language Integrated Queries).

Trataremos algo más de esta problemática más adelante en otro artículo.

El segundo caso (gestión de tareas en paralelo) trata toda las problemáticas que pueden derivarse de la ejecución simultánea de diferentes métodos, cada uno con sus parámetros y su código específicos. Ya hemos visto algo de esto en la primera parte de esta serie de artículos (la sintaxis será la misma), pero en estos casos veremos también cómo programar métodos que se ejecuten simultáneamente con otras secciones de la aplicación.

Ejecución de un método en un hilo secundario

Tal como apunté en el capítulo anterior, la forma recomendada para iniciar un método en un hilo diferente al actual (tener en cuenta que no es la única) será empleando el método estático

Task.Factory.StartNew<T> (…);

Este método tiene bastantes variantes pero básicamente (las más empleadas) tomarán como parámetro de entrada un delegado a un método (una función  – delegado ‘Function’ – o una acción – delegado ‘Action’ ) que será el que se ejecute en paralelo.

El método StartNew<T> se encargará de crear un objeto Task<T> y de obtener y asignar un hilo de ejecución para el método suministrado y asignar toda esta información al objeto Task y por supuesto de iniciar el método (iniciar la tarea). La tarea de segundo plano no se inicia inmediatamente sino que se encola para su ejecución y el programador de tareas (TaskScheduler) se encargará de sacar la tarea preparada de la cola, cuando se pueda o corresponda, e iniciarla.

(Un objeto Task, como comenté en el artículo anterior, representa una tarea – un método o conjunto de métodos – que  se está ejecutando en nuestra aplicación, de tal forma que podamos obtener información de la tarea, saber si ha terminado o está en ejecución todavía, obtener un objeto de sincronización para esperar a que la tarea termine y en algunos casos, terminar la tarea forzando la terminación antes de que ésta termine por su cuenta).

En muchos casos, es habitual, en lugar de suministrar el delegado al método a ejecutar, emplear una expresión Lambda en su lugar. En este caso, el compilador creará un método privado dinámico y suministrará el delegado a este método al método StartNew.

 

bool TareaSegundoPlano(object objState)

{

       System.Diagnostics.Debug.WriteLine(“Tenemos acceso al dispatcher de este objeto = {0}”, this.Dispatcher.HasThreadAccess);

       return true;

}

private async void cmdTareasSegundoPlano_Click_1(object sender, RoutedEventArgs e)

{

       bool b = await Task.Factory.StartNew<bool>(TareaSegundoPlano, null);

       System.Diagnostics.Debug.WriteLine(“Hemos terminado de ejecutar la tarea de segundo plano”);

       //

       int i = 3;

       b = await Task.Factory.StartNew<bool>((objState) =>

       {

             System.Diagnostics.Debug.WriteLine(“Tenemos acceso al dispatcher de este objeto = {0} {1}”, this.Dispatcher.HasThreadAccess, i);

             return false;

       }, null);

       System.Diagnostics.Debug.WriteLine(“Hemos terminado de ejecutar la tarea de segundo plano”);

}

 

En el código de ejemplo anterior, hemos asignado al evento ‘click’ de un botón el método (gestor de eventos) cmdTareasSegundoPlano_Click_1. (Segundo método).

Este método ejecutará a su vez un método (de dos maneras diferentes en el ejemplo) en un hilo de segundo plano (hilo diferente al del interfaz de usuario sobre el que se está ejecutando el gestor de eventos).

En el primero de los dos casos, empleamos un delegado al método TareaSegundoPlano (delegado generado automáticamente por el compilador) para ejecutar el método StartNew<bool>. Empleamos la versión del método StartNew que requiere como parámetros una función (Function<bool>) y un segundo parámetro que puede ser un objeto de datos adicional, que se pasará al método a ejecutar en segundo plano (en el ejemplo pasamos – en ambos casos – el valor null).

En el segundo caso empleamos una expresión Lambda para escribir el método in-line. Como puede verse en este ejemplo, la expresión Lambda podría tener alguna ventaja con respecto al delegado en algunos casos, ya que nos permitiría tratar variables locales adicionales del método (captura de variables externas), en el ejemplo la variable (i), además de los parámetros que se pueden suministrar al método de la primera forma – delegado-, pero quizás – como puede verse en el código – la lectura / escritura del código sea más farragosa, sobre todo si el contenido de la expresión Lambda es largo o complejo(evidentemente no es el caso del ejemplo). Es simplemente un tema de gustos.

Como puede verse en el código, en ambos métodos se ha trazada un texto que indica si el hilo en el que se está ejecutando nuestro método tiene acceso al dispatcher de la página desde la que ha sido invocada. En ambos casos el resultado es ‘false’ (no tenemos acceso al dispatcher), ya que estamos ejecutándonos en otro hilo diferente al del interfaz de usuario, luego no podremos acceder directamente a los controles que estén incluidos en la página ni a ningún código que actualice estos controles sin que se produzca una excepción de acceso no permitido al hilo del interfaz de usuario.

Como puede observarse si se ejecuta el código de ejemplo, primero se ejecutará la primera tarea en segundo plano (el método esperará a que la tarea termine gracias a la expresión await) y después se ejecutará la segunda tarea e igualmente el método esperará a que la tarea termine. Se ha tenido que añadir el modificador async al gestor de eventos () para poder esperar a las dos tareas de segundo plano con la expresión await.

Si no necesitáramos conocer el resultado de la ejecución del método de segundo plano (y tampoco esperar a que termine) podríamos escribir el método de segundo plano como del tipo ‘Action’ (sin parámetros y sin valor de retorno – void Metodo ()). En este caso, no es siempre seguro intentar esperar a que el método termine (dependerá de cómo esté escrito el método) y normalmente se empleará para métodos que se ejecutarán en segundo plano durante largo tiempo (ver más abajo) de forma relativamente independiente con otros métodos.

 

En el caso anterior de ejecución de varios métodos diferentes (aunque lo hayamos expresado con un método y con una expresión Lambda) con la sintaxis estándar – tal como lo he apuntado – los métodos se ejecutarán en paralelo con el método actual, pero no en paralelo entre ellos. Primero se ejecuta uno, esperamos a que termine y después se ejecuta el otro e igualmente esperamos a que termine.

Ejecución de varios métodos de segundo plano simultáneamente

Si quisiéramos ejecutar dos o más métodos de segundo plano en paralelo (y esperar a que terminen – todos o alguno),  se puede emplear la siguiente construcción.

Para esperar a que terminen todas las tareas emplearemos el método Task.WaitAll.

Ejemplo:

bool MetodoLogico1()

{

       System.Diagnostics.Debug.WriteLine(“>>> Ejecutando MetodoLogico1”);

       // TODO: El resto del método

       return true;

}

bool MetodoLogico2()

{

       System.Diagnostics.Debug.WriteLine(“>>> Ejecutando MetodoLogico2”);

       // TODO: El resto del método

       return true;

}

int Metodo3()

{

       System.Diagnostics.Debug.WriteLine(“>>> Ejecutando Metodo3”);

       // TODO: El resto del método

       System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();

       do

       {

       } while (sw.ElapsedMilliseconds < 5 * 1000);

       //

       return 1;

}

 

private async void cmdTareasParalelas_Click_1(object sender, RoutedEventArgs e)

{

       EjecutarTareasParalelas(); // Caso 1

       //

       await Task.Factory.StartNew(EjecutarTareasParalelas); // Caso2

       //

       System.Diagnostics.Debug.WriteLine(“EjecutarTareasParalelas terminado”);

}

 

void EjecutarTareasParalelas()

{

       System.Threading.Tasks.Task<bool> tb1;

       Task<bool> tb2;

       Task<int> tb3;

       //

       tb1 = Task.Factory.StartNew<bool>(MetodoLogico1);

       tb2 = Task.Factory.StartNew<bool>(MetodoLogico2);

       tb3 = Task.Factory.StartNew<int>(Metodo3);

       Task.WaitAll(tb1, tb2, tb3);

// el hilo actual se quedará ‘bloqueado’ mientras no terminen todos los métodos

       //

       System.Diagnostics.Debug.WriteLine(“Terminados todos los métodos”);

       // Si quisiéramos obtener el resultado de los métodos

       bool r1 = tb1.Result;

       bool r2 = tb2.Result;

       int r3 = tb3.Result;

}

 

En este método, desde un gestor del evento click de un botón invocamos el método EjecutarTareasParalelas de dos formas diferentes para comprobar el diferente funcionamiento.

El método ‘EjecutarTareasParalelas’ crea las tres tareas con el método StartNew como hemos visto anteriormente y emplea el método WaitAll para esperar a que todas terminen.

En el ejemplo hemos puesto una de las tres tareas que simula una duración de 5 segundos.

Si invocamos directamente (desde el hilo del interfaz de usuario) (Caso 1) el método ‘EjecutarTareasParalelas’ comprobaremos que el interfaz de la aplicación se queda bloqueado durante los 5 segundos que dura la ejecución de la tarea más larga.

Cuando se invoca el método ‘EjecutarTareasParalelas’ mediante la expresión await StartNew (Caso 2) el hilo del interfaz de usuario queda liberado mientras esperamos a que termite el método y se reanudará, escribiendo la traza “EjecutarTareasParalelas terminado”, una vez que haya terminado la espera.

Si únicamente necesitamos que alguna de ellas termine, se puede emplear el método (WaitAny) en lugar de (WaitAll).

 

 

Tipo System.Threading.Thread  y Windows RT / Tareas de larga duración

Hasta ahora, la forma recomendada de gestionar una tarea de segundo plano de larga duración – por ejemplo una tarea que estuviera continuamente en funcionamiento mientras la aplicación se ejecuta – era empleando un objeto Thread (del espacio de nombres System.Threading), en contraposición al método (ThreadPool.QueueUserWorkItem ()) que era el recomendado para una tarea de corta duración.

Las tareas gestionadas con un objeto de tipo Task estaban también reservadas para tareas de corta duración por ejemplo una tarea en paralelo (segundo plano) para actualizar una tabla de una Base de Datos mientras el hilo principal de la petición (de la aplicación, probablemente en una aplicación de cliente) se dedicaba a atender al cliente (gestionar el interfaz de usuario, completar la operación en curso u otras labores).

Crear, iniciar y monitorizar un hilo de ejecución (Thread) de esta manera requería una serie de pasos con una relativa complejidad y un conocimiento bastante importante de lo que se estaba haciendo. Por ejemplo:

 

Para crear e iniciar un hilo de esta manera requería un código como este.

      Thread m_workerThread;

      m_workerThread = new Thread (new ThreadStart (Listen));

 

      m_workerThread.Start ();

 

y para detener la tarea  … de forma forzada.

      m_workerThread.Abort ();

      m_workerThread.Join (30 * 1000);

 

El tipo System.Threading.Thread ha sido suprimido en la versión de .NET Framework para Windows RT (no así en la versión estándar del Framework 4.5 para una aplicación estándar Windows en Visual Studio 2012) y en su lugar se debería emplear los objetos y métodos que tratan con tareas (Task) tal como se está viendo en este artículo.

De esta manera para iniciar una tarea de larga duración en Windows RT el código sería algo como esto:

System.Threading.CancellationTokenSource m_src;

Task m_tskTareaLargaDuracion;

 

 

void TareaInfinita(object objData)

{

       CancellationToken cToken = m_src.Token;

 

       for (int n = 0;;n++)

       {

             System.Diagnostics.Debug.WriteLine(“*** {0}”, n);

             // Primera forma de comprobar si hay que cancelar una tarea

             //cToken.ThrowIfCancellationRequested();

             // Segunda forma de comprobar si hay que cancelar una tarea

             if (cToken.IsCancellationRequested)

             {

                    // Realizar operaciones de limpieza si fuera necesario

                    System.Diagnostics.Debug.WriteLine(“<<< Terminando tarea de larga duración”);

                    //

                    return;

             }

       }

       //

       //System.Diagnostics.Debug.WriteLine (“Hemos terminado la tarea”);

}

 

private void cmdTareaLargaDuracion_Click_1(object sender, RoutedEventArgs e)

{

       // Iniciar tarea(s) de larga duración

       //

       m_src = new System.Threading.CancellationTokenSource();

       m_tskTareaLargaDuracion = Task.Factory.StartNew(TareaInfinita, (object) null, m_src.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

       //

}

 

En el código de ejemplo anterior (método ‘cmdTareaLargaDuracion_Click_1’) iniciaremos una tarea, contenida en el método ‘TareaInfinita’ que realizará una tarea infinita, hasta que la aplicación indique que hay que terminarla. (Podría ser por ejemplo una tarea de monitorización o una tarea de comunicaciones que está continuamente – y durante toda la ejecución de la aplicación – recibiendo o enviado datos o consultando periódicamente algún recurso).

Empleamos la versión del método StartNew con todos los parámetros posibles: Delegado al método que hay que ejecutar con la tarea, objeto de datos adicionales, testigo de cancelación de la tarea, opciones de creación de la tarea y objeto programador de tareas.

En este caso pasaremos al método la opción de creación de una tarea ‘TaskCreationOptions.LongRunning’ que indica justamente lo que parece: que la tarea va a ejecutarse durante largo tiempo. De esta manera el programador de tareas estándar, empleará un hilo dedicado específico para esta tarea en lugar de obtenerlo del grupo de hilos del sistema (ThreadPool) – que es forma estándar de asignar un hilo a una tarea si no empleamos esta opción.

Almacenamos el objeto Task retornado por el método en una variable de la página (clase derivada de Page) ‘m_tskTareaLargaDuracion’ para poder trabajar posteriormente con este objeto.

Cancelar una tarea de segundo plano

La cancelación de una tarea de segundo plano es una labor colaborativa entre la aplicación que ha lanzado la tarea y la propia tarea.

Normalmente se emplea un objeto CancellationToken  (struct CancellationToken)  obtenido mediante un objeto CancellationTokenSource (como puede verse en el ejemplo anterior) que permite gestionar la cancelación de una tarea (o tareas) de una forma organizada.

El siguiente código muestra la forma de cancelar la tarea iniciada anteriormente, desde un método invocado en este caso por un botón (no suele ser la forma más habitual).

private void cmdCancelarTarea_Click_1(object sender, RoutedEventArgs e)

{

       // Indicamos que hay que cancelar las tareas que estén atendiendo a este testigo de cancelación

       m_src.Cancel();

       bool b = m_tskTareaLargaDuracion.Wait(3 * 1000);

       //

       txtResultado.Text = string.Format(“Tarea de larga duración terminada correctamente = {0}”, b);

}

 

El objeto CancellationTokenSource tiene un método Cancel que hace que el testigo indique que se ha solicitado una cancelación de las tareas que estén atendiendo al testigo.

Tal como he comentado anteriormente, la cancelación de la tarea es colaborativa, esto quiere decir que la tarea debe estar pendiente del testigo de cancelación adecuado para saber cuándo tiene que terminar y terminar por su cuenta. Si la tarea no realiza esta comprobación, no habrá forma de detenerla o incluso podría ser que aunque esté atenta al testigo de cancelación no pueda o no quiera terminar.

En el ejemplo se muestran dos formas en que una tarea puede atender al testigo de cancelación:

·         Consultando periódicamente la propiedad ‘IsCancellationRequested’ del testigo de cancelación (como puede verse en el código de ejemplo), lo que permite realizar una parada controlada

·         Invocando el método ‘ThrowIfCancellationRequested’ del testigo de cancelación, lo que produciría una excepción del tipo ‘OperationCanceledException cuando la tarea deba acabar.

En el código de ejemplo, la aplicación queda a la espera durante 3 segundos (3000 milisegundos) una vez que se ha indicado a la tarea que se detenga. Si la tarea se detiene en el tiempo indicado, el método Wait retornará ‘true’. Si la tarea no se ha detenido retornará ‘false’.

 

En un próximo artículo intentaré exponer una problemática completa de una aplicación de monitorización remota que hace un uso extensivo de todas estas técnicas y problemáticas.

 

Más información

(Noviembre 2012-Marzo 2013)

Este y otros temas sobre diseño y programación de aplicaciones en Windows RT / Windows 8 serán tratados en los seminarios para empresas (para el ámbito de Bizkaia / País Vasco) que hemos preparado para los próximos meses. Más información en Seminarios.

 

Anuncios

Programación asíncrona con C# 5.0 y Microsoft .NET Framework 4.5

 

NAG 2012.10.30

Presentación

Una de las novedades más importantes que se han introducido en la última versión de .NET Framework  (4.5) y de los lenguajes de la plataforma de desarrollo Visual Studio (C# y Visual Basic) es la integración en el propio lenguaje de un modelo de programación asíncrono.

La programación asíncrona se viene empleando habitualmente, principalmente desde la aparición de Windows Forms en .NET (previamente el modelo de programación asíncrona estaba reservado casi en exclusiva a las aplicaciones escritas en C / C++).

En general el modelo de programación asíncrona está orientado a sacar más partido (principalmente en aplicaciones de cliente) de los nuevos modelos y tipos de microprocesadores con múltiples núcleos.  El problema al que nos enfrentábamos hasta ahora era que este tipo de programación era relativamente compleja y muy diferente del modelo síncrono de programación (el modelo más habitual), de ahí que no fuera tan habitual.

El nuevo modelo de programación incluido en la última versión de C# (y Visual Basic) y de .NET Framework trata de simplificar y aunar los dos modelos (hacer que los dos modelos sean lo más parecidos posible). Este nuevo modelo no es la única manera de afrontar la problemática de la programación asíncrona en .NET, pero el modelo propuesto es el más sencillo y el más recomendable en la mayoría de los casos.

La programación asíncrona ha pasado de ser recomendable en algunos tipos de aplicaciones, –  una opción más -, a ser una necesidad, ya que unos cuantos de los métodos que tendremos que invocar en nuestras aplicaciones del nuevo .NET Framework, son, en esta nueva versión, métodos asíncronos, como podremos ver en los ejemplos incluidos en el artículo.

 

En el resto del artículo mostraré de forma simplificada, sin entrar en excesivo detalle ni complejidades, el funcionamiento y uso del nuevo modelo de programación asíncrona.

¿Qué es un método asíncrono?

Un método asíncrono es aquel que puede seguir ejecutándose (o más correctamente quizás, puede que no haya terminado de ejecutarse completamente) después de haber devuelto el control al método que lo ha invocado. (Hay que tener en cuenta que he remarcado puede seguir ejecutándose, lo que indica que el método también podría haber terminado de forma síncrona).

En general un método asíncrono tendrá un prototipo similar a esto (especificado en C#):

async Task<T> NombreMetodoAsync ([parámetros]);

El nuevo modificador async (por delante del tipo de valor de retorno del método) indica que este método puede ejecutarse de forma asíncrona. Normalmente se suele añadir al final del nombre del método (no es necesario en absoluto pero es recomendable para reconocer el método como asíncrono dado únicamente su nombre, por ejemplo al leer el código) el sufijo Async (en inglés, aunque lo podríamos ajustar a nuestro idioma preferido).

NOTA: Hay que tener el cuenta que el modificador async no hace que un método tenga un comportamiento asíncrono sino que le indica al compilador que debe poder generar el código de un método asíncrono si hace falta.

El método asíncrono normalmente retornará un objeto del tipo Task (un objeto que indica una tarea en marcha) y más normalmente aún un tipo genérico del tipo Task<T> siendo T el tipo del valor lógico retornado por ese método (por ejemplo Task<int> para indicar que el método retornará un valor de tipo (int) entero o Task<bool> para indicar que el método retornará un valor de tipo bool, etc.).

Contenido del un método asíncrono

Realmente un método con el modificador async se convierte en método asíncrono si en su cuerpo (contenido del método) existe alguna expresión await.

 

Una expresión await solamente puede aparecer dentro de un método async.

 

 

Por ejemplo, teniendo en cuenta que el sistema de archivos de Windows RT (sistema de archivos aislado) define unos métodos que tienen un funcionamiento asíncrono (por ejemplo el método CreateFileAsync o el método ReadTextAsync – como puede intuirse por sus nombres), la forma de realizar lecturas y escrituras en uno de estos archivos puede ser de la siguiente manera:

async Task<bool> _AbrirArchivo(string nombreArchivo)

{

        StorageFile archivo;

        StorageFolder storageFolder = KnownFolders.DocumentsLibrary;

 

        string strNombreArchivo = nombreArchivo;

 

        try

        {

                archivo = await storageFolder.CreateFileAsync(strNombreArchivo, CreationCollisionOption.ReplaceExisting);

 

                await FileIO.WriteTextAsync(archivo, “un texto de prueba\nen dos líneas”);

                string str = await FileIO.ReadTextAsync(archivo);

                txtResultado.Text = str;

        }

        catch (Exception /*exc*/)

        {

                return false;

        }

        //

        System.Diagnostics.Debug.WriteLine(“<<< _AbrirArchivo terminando”);

        return true;

}

 

Este método de ejemplo, abre (o crea) un archivo en la carpeta de documentos de usuario, escribe un contenido en el archivo, lee el contenido del archivo y lo muestra en un control de tipo TextBlock (txtResultado).

Hasta el momento de ejecutarse la primera expresión await (en este caso es el método ‘CreateFileAsync’, que crea o abre un archivo dentro de la carpeta (StorageFolder) de documentos de usuario, pero puede ser cualquier método asíncrono) nuestro método se estará ejecutando de forma síncrona. Si el método que acompaña a la expresión await realmente se ejecuta de forma asíncrona (se comprueba que la tarea haya completado o no), nuestro método iniciará su comportamiento asíncrono.

El comportamiento asíncrono implica, crear un objeto de tipo Task<T> (en nuestro ejemplo Task<bool>) y asignar el resto del método que se está ejecutando – el método async-, como un submétodo de la tarea (un delegado del tipo Action) y se retornará el control al llamador (suspendiendo el método asíncrono en ejecución hasta que la tarea asíncrona de la expresión await termine). El funcionamiento real del método async es algo más complejo que todo esto (ver el documento “Asynchronous Functions en C#”), pero lo anteriormente expuesto puede valer como resumen.

En el caso de un método asíncrono sin valor de retorno (ver clasificación más abajo), si el método invocado en la expresión await no ha concluido todavía, se crea igualmente el delegado pero no se retornará la tarea asociada.

Tipos de métodos asíncronos

Básicamente podríamos dividir los métodos asíncronos (por su definición) en dos grupos:

·         Métodos a los que podemos esperar

·         Métodos a los que no podemos esperar

Los métodos a los que podemos esperar (awaitable en inglés) retornarán un objeto de tipo Task o más normalmente Task<T>.

Los métodos a los que no podemos esperar, estarán marcados como async pero no retornarán ningún valor (void).

Si invocamos un método al que podemos esperar sin una expresión await, el compilador nos indicará (normalmente una advertencia) que podríamos esperar a la terminación del método si quisiéramos, aunque no es obligatorio. En este caso, la ejecución del resto del método continuará sin esperar a la tarea asíncrona invocada.

Ejemplos

El siguiente código muestra un ejemplo más completo de uso de los métodos asíncronos:

private void cmdAbrirArchivo_Click_1(object sender, RoutedEventArgs e)

{

        AbrirArchivo();

        //

        System.Diagnostics.Debug.WriteLine (“<<< cmdAbrirArchivo_Click_1 terminando”);

}

 

async void AbrirArchivo()

{

        bool b = await _AbrirArchivo(“prueba1.txt”);

        //

        System.Diagnostics.Debug.WriteLine(“<<< AbrirArchivo terminando”);

}

 

async Task<bool> _AbrirArchivo(string nombreArchivo)

{

        StorageFile archivo;

        StorageFolder storageFolder = KnownFolders.DocumentsLibrary;

 

        string strNombreArchivo = nombreArchivo;

 

        try

        {

                archivo = await storageFolder.CreateFileAsync(strNombreArchivo, CreationCollisionOption.ReplaceExisting);

 

                await FileIO.WriteTextAsync(archivo, “un texto de prueba\nen dos líneas”);

                string str = await FileIO.ReadTextAsync(archivo);

                txtResultado.Text = str;

        }

        catch (Exception /*exc*/)

        {

                return false;

        }

        //

        System.Diagnostics.Debug.WriteLine(“<<< _AbrirArchivo terminando”);

        return true;

}

 

Estos tres pequeños métodos de ejemplo, muestra algunas de las posibilidades de la programación asíncrona.

El primer método (cmdAbrirArchivo_Click_1) es un gestor de eventos del Click de un botón que tenemos en una de nuestras páginas. No está marcado como async (aunque puede ser necesario en otros casos y se podría especificar) porque, en este caso, no requiere emplear una expresión await (no va a poder – o no quiere – esperar a un método asíncrono).

El segundo método (AbrirArchivo) es un método asíncrono (puede esperar a una tarea asíncrona y suspender la ejecución de parte de su código hasta que esta tarea (o tareas) terminen) pero no retorna una tarea (Task) luego no es un método al que podamos esperar.

El tercer método (_AbrirArchivo), es un método asíncrono que retorna un objeto del tipo Task<bool> lo que indica que desde un punto de vista lógico retornará un valor bool (true o false). Como retorna un objeto de tipo Task, es un método al que podemos esperar y la sintaxis de llamada a este método (como se ve en el ejemplo) puede ser esta:

        bool b = await _AbrirArchivo(“prueba1.txt”);

 

Aunque también puede ser esta otra:

        Task<bool> tb = _AbrirArchivo(“Prueba2.txt”);

 

En este segundo caso, obtenemos por nuestra cuenta una referencia al objeto tarea (Task) que retorna el método, y que la emplearemos para lo que necesitemos (es un caso más flexible y completo, pero normalmente no necesario).

Este método requiere una sincronización estricta para realizar las tareas en un orden lógico, que sería: Crear o abrir el archivo, escribir y finalmente leer. Por esta razón realiza una espera (await ) de todos los métodos invocados hasta que terminen su ejecución. Espera a que termine el método de creación antes de intentar escribir en el archivo, ya que sería imposible escribir o leer en el archivo sin tener una referencia al archivo (que vendrá como valor lógico de retorno del método CreateFileAsync). Posteriormente escribe en el archivo y espera hasta que la escritura se complete y posteriormente inicia la lectura del contenido del archivo y una vez leído el contenido lo muestra en el TextBlock. Sin las expresiones await el método este no se ejecutaría correctamente o al menos no en el orden previsto.

 

Los tres métodos, como se puede observar en el código de ejemplo, escriben una traza en la salida de depuración (Debug.WriteLine) y el resultado de estas trazas, como cabe esperar, será:

<<< cmdAbrirArchivo_Click_1 terminando

<<< _AbrirArchivo terminando

<<< AbrirArchivo terminando

 

Primero se completa el método del gestor de eventos del Click (ya que es un método que no espera a que la parte asíncrona termine y posteriormente (y en este orden) terminarán el método más interno (_AbrirArchivo) y posteriormente el método más externo (AbrirArchivo) ya que este último espera a que el método interno termine (await _AbrirArchivo()).

Hay que tener en cuenta, que en todos estos casos el resto del método después de la expresión await se ejecutará en el mismo hilo (en el mismo contexto) que en el que se estaba ejecutando el método originalmente cuando fue suspendido en espera de la terminación del método asíncrono invocado, aunque probablemente los métodos asíncronos a los que hemos invocado hayan creado otros hilos de ejecución por su cuenta (cosa muy probable, pero en principio desconocida para nosotros).

Control de excepciones

Un aspecto importante de este modelo de programación asíncrona es la correcta gestión de las excepciones producidas durante una expresión await.

Si el método invocado produjera una excepción no controlada (o la controlara pero la volviera a reproducir), esta excepción puede ser tratada de la forma habitual mediante una construcción try … catch como se puede ver en el ejemplo anterior (método _AbrirArchivo).

Tareas ejecutadas en otros hilos

Si necesitáramos crear una tarea que se ejecutara en otro hilo de ejecución (Thread) – ejecución en paralelo – (normalmente obtenido del grupo de hilos del sistema – ThreadPool) podríamos emplear la sintaxis:

Task.Factory.StartNew (…)

 

En este caso sí que se obtendrá un nuevo hilo de ejecución para la función especificada como parámetro del método (). Hay que tener en cuenta en estos casos que la invocación de un código que actualice el interfaz de usuario desde el método iniciado con el método StartNew, requerirá sincronización con el dispatcher de los objetos visuales y no puede realizarse directamente.

 

 

Aspectos avanzadas

En dos próximos artículos (ya en preparación) trataré con más detalle aspectos más avanzados de la programación asíncrona y temas de operaciones temporizadas en Windows RT.