sábado, 14 de junio de 2014

Receta No. 4-2 en C#: Ejecución de un Método Asincrónicamente

Tabla de Contenido

0. Introducción
1. Problema
2. Solución
3. Discusión de la Solución
3.1 Sincronización vs asincronización
3.1.1 Invocación de método sincrónica
3.1.2 Invocación de método asincrónica
3.2 Manejo de threads por parte de .NET Framework
3.3 Métodos BeginInvoke y EndInvoke
3.3.1 BeginInvoke
3.3.2 EndInvoke
3.3.2.1 Blocking
3.3.2.2 Polling
3.3.2.3 Waiting
3.3.2.4 Callback
4. Práctica: Código Fuente C#
5. Conclusiones
6. Glosario
7. Literatura & Enlaces

0. Introducción

En esta oportunidad para la preparación de una nueva receta vamos a incursionar en el proceso de ejecución de un método de asincrónicamente. Veremos que esta enfoque de ejecución de tiempo compartido (time slice) facilita el diseño de programas que responden mejor a requerimientos de alto desempeño (gracias al aprovechamiento de los recursos del sistema). Para empezar conoceremos la diferencia entre el concepto sincrónico y asincrónico; enseguida, la forma en que el Framework .NET realiza las tareas delegadas en paralelo; más adelante, discutiremos algunas limitaciones en el uso de threads; y para finalizar presentaré los patrones de control de llamadas (invocaciones) de métodos de forma asincrónica.

1. Problema

Debemos diseñar un modelo de ejecución de piezas de código que funcione en paralelo. El primer requerimiento es facilitar la ejecución de tareas en un segundo plano a través de un thread separado para cada una. Por otro lado, el segundo requerimiento consiste en obtener información de los cálculos generados por la ejecución de esa tarea una vez finalice.

2. Solución

El propio Framework .NET posee artefactos que facilitan la ejecución de piezas de código en paralelo. Los delegados (Delegados en C#) juegan un papel importante, gracias a la posesión de dos métodos que facilitan la ejecución en paralelo, y la obtención de resultados: BeginInvoke, y EndInvoke.

3. Discusión de la Solución

Antes de empezar a discutir el funcionamiento de ejecución de métodos de manera asincrónica, exploraremos varios conceptos claves para facilitar la comprensión del tema principal de esta receta.

3.1 Sincronización vs Asincronización

3.1.1 Invocación de método sincrónica

Como mencioné en la receta anterior (Ejecución de un Método sobre un Pool de Threads), la forma clásica de invocación de métodos es sincrónica. En este tipo de invocación, el tiempo que toma la ejecución de las sentencias que componen un bloque de código es la suma de los tiempos que le toma a cada sentencia ejecutarse. Asumamos que tenemos el siguiente método:

public void Metodo()
{
// Suspensión de la ejecución por 10 segundos:
Thread.Sleep(10000);
}

Simulamos con la sentencia Thread.Sleep(10000) que el tiempo que ha tomado en ejecutarse el método Metodo son 10 segundos. Si este método es ejecutado, por ejemplo, 50 veces, el thread de ejecución principal (pensemos el propio de Main) que invoca a este método deberá esperar 500 segundos (aproximadamente 8 minutos) para continuar con la siguiente instrucción.


Este es el grupo de tareas que se realizan por cada invocación del método Metodo:
  1. Invocación del método Metodo.
  2. El grupo de sentencias del método Metodo se ejecutan.
  3. Metodo retorna al punto de invocación del thread que le invocó.
En los artículos acerca de delegados aprendimos que un delegado puede encapsular uno o varios métodos, siempre y cuando estos posean la misma firma (e inclusive el tipo de retorno) que posee el delegado. Esta es la forma en que declaramos un encapsulado, encapsulamos un método, e lo invocamos a través del método Invoke:

delegate void Delegado();

// Declaración del delegado, y encapsulación del
// método `Metodo`:
Delegado del = new Delegado (Metodo);

// Invocación indirecta de `Metodo`:
del.Invoke();


A pesar de que hemos encapsualdo el método Metodo en una instancia de un delegado, la invocación a través de Invoke continua siendo sincrónica.

3.1.2 Invocación de método asincrónica

La invocación de un método de modo asincrónico consiste en la asignación de un thread desde el pool de threads (estructura que permite poner en cola un conjunto de métodos que serán ejecutados en paralelo) a un método, de tal manera que una vez invocado el método, el thread principal continue ejecutando las siguientes sentencias sin detenerse (técnicamente, bloquearse) a que finalice la ejecución del método.


En C#, esto lo podemos hacer a través del método de delegados BeginInvoke. Este es un ejemplo de uso:


// Declaración del delegado, y encapsulación del
// método `Metodo`:
Delegado del = new Delegado (Metodo);

// Infocación del método `Metodo` asincrónicamente:
for ( int i = 0; i < 50; ++i)
{
del.BeginInvoke (null, null);
}


Por cada iteración del ciclo for, se invocará de forma asincrónica el método Metodo a través del método BeginInvoke (por ahora podemos obviar los argumentos pasados). Debemos tener claro que cuando utilizamos esta técnica de invocación de métodos, determinar en qué momento o en qué orden finaliza la ejecución de las 50 invocaciones no es una tarea que está bajo nuestro control. Los resultados varían por cada prueba de ejecución qurealicemos (como en el ejemplo de cálculo de la serie Fibonacci de la sección 4 de la receta Ejecución de un Método sobre un Pool de Threads.

3.2 Manejo de threads por parte de .NET Framework

Detrás de cámaras .NET Framework se encarga de asignar a cada invocación asincrónica de un método un thread, evitando con esto el bloque del thread que le invocó, y, permitiendo la continuación con la ejecución de las sentencias siguientes. Este conjunto de threads disponibles para la llamada (termino intercambiable con invocación) asincrónica se hayan en lo que se conoce como .NET Thread Pool. Algunos puntos a tener en cuenta sobre este pool de threads:
  • Cada invocación asincrónica de un método es asignada a un thread distinto.
  • El número total de threads disponibles en .NET Thread Pool es de 25 (este límite puede ser cambiado, sin embargo es importante considerar un límite sobre el ambiente de ejecución [servidor de base de datos SQL Server, servidor Web IIS] que puede estar impuesto por las capacidades de cómputo [memoria y/o procesador]).
  • Superado el límite de threads de .NET Thread Pool, los nuevos métodos que entran a la cola, deberán esperar a que se libre uno de los threads ocupados para empezar su ejecución. Este proceso se conoce originalmente como Thread Pool Starvation.

3.3 Métodos BeginInvoke y EndInvoke

Ya hemos nombrado uno de los miembros de un delegado que permite la invocación asincrónica de un método: BeginInvoke. Otro miembro es EndInvoke. Con este ultimo podemos obtener la el valor de retorno calculado al final de la ejecución (en pocos minutos ampliamos este método en la sección de técnicas de determinación de completitud de un método.

3.3.1 BeginInvoke

De acuerdo con [1], el método BeginInvoke incluye los mismos argumentos que los especificados por la firma del delegado, además, de dos argumentos adicionales de soporte asincrónico. Estos son:
  1. Instancia del delegado System.AsyncCallback [5] que hace referencia a un método de respuesta (callback). Útil para responder a tareas una vez finalizada la ejecución del método asincrónico.
  2. Objeto contenedor de información útil para el método de respuesta (callback).

3.3.2 EndInvoke

Mencioné que este método nos permite obtener información del método asincrónico una vez que este finalice. A este método se asocian técnicas de determinación de completitud asincrónica, es decir, conocer el instante de finalización de la llamada de un método asincrónico.

A continuación enumero estas técnicas, que en lo personal considero que deben ser conocidas para en cierta medida podamos controlar/conocer el instante de finalización de la llamada asincrónica de un método, y poder usar la información que trae de regreso como fuente de datos para otras operaciones.

3.3.2.1 Blocking

Con esta técnica se bloquea el thread de ejecución actual hasta que el método asincrónico complete su ejecución. Esto suena a invocación sincrónica; empero, esta técnica permite controlar el momento en que el thread de ejecución actual deba bloquearse: esto nos daría la flexibilidad para la especificación de sentencias de procesamiento antes de que se entre en estado de bloqueo.

Archivo de código fuente Blocking.cs [enlace alternativo]:

En la líneas 8-13 declaramos el método ProcesoLargo que posee la lógica para introducir un retraso de ejecución simulado y retornar una cadena compuesta por valores calculados (thread actual del dominio de aplicación).


En la línea 15 declaramos el delegado Delegado. Así:

delegate string Delegado (int tiempoRetraso, out int threadEjecucion);


Sobre la línea 21 creamos una instancia de este delegado, y encapsulamos el método ProcesoLargo. Iniciamos la invocación asincrónica en la sentencia de la línea 31. Los argumentos de BeginInvoke son los siguientes:
  • 3000: tiempo de retraso que debe simularse en el método ProcesoLargo.
  • threadEjecucion: hilo sobre el que se ejecutó el método ProcesoLargo.
  • null: Instancia null de AsyncCallback.
  • null: ningún contenedor de datos.
Con la sentencia de la línea 35 se simula la ejecución de una tarea previa a la finalización de la invocación asincrónica de ProcesoLargo. Ya en la línea 39 usamos el método EndInvoke (y el thread principal se bloquea hasta finalizar ProcesoLargo). El resultado es una instancia string que se localiza en la variable cadena.

Compilación:


  1. csc /target:exe Blocking.cs

Ejecución assembly:


  1. .\Blocking.exe

> Prueba de ejecución.

Resutado:
Esta sentencia se ejecutó mientras el método `ProcesoLargo` está en ejecución.
Valor de retorno del delegado "Tiempo de retraso: 3000" sobre el thread -1251103888

3.3.2.2 Polling


Consiste en realizar repetidamente la consulta de la finalización de ejecución de un método asincrónico. En [1] nos aclaran que esta técnica puede resultar simple, sin embargo ineficiente desde el punto de vista de rendimiento: consumo de tiempo de procesador. Adiconalmente,
"...Because polling involves maintaining a loop, the actions of the waiting thread are limited, but you can easily update some kind of progress indicator."
Archivo de código fuente Polling.cs [enlace alternativo]:

En la líneas 8-13 declaramos el método ProcesoLargo que posee la lógica para introducir un retraso de ejecución simulado y retornar una cadena compuesta por valores calculados (thread actual del dominio de aplicación).


En la línea 15 declaramos el delegado Delegado. Así:

delegate string Delegado (int tiempoRetraso, out int threadEjecucion);


Sobre la línea 21 creamos una instancia de este delegado, y encapsulamos el método ProcesoLargo. Iniciamos la invocación asincrónica en la sentencia de la línea 31. Los argumentos de BeginInvoke son los siguientes:
  • 3000: tiempo de retraso que debe simularse en el método ProcesoLargo.
  • threadEjecucion: hilo sobre el que se ejecutó el método ProcesoLargo.
  • null: Instancia null de AsyncCallback.
  • null: ningún contenedor de datos.
Con el ciclo while (líneas 34-38) realizamos la consulta (polling) con la propiedad IsCompleted [6]. Mientras que su valor calculado sea false se mostrará el testo Polling... y se generará un retraso de 100 milisegundos por cada iteración.


Cuando el ciclo anterior se rompa, se obtiene el resultado de la ejecución asincrónica (línea 40).

Compilación:


  1. csc /target:exe Polling.cs

Ejecución assembly:


  1. .\Polling.exe

Resultado:
Prueba de ejecución uso polling
Figura 1. Prueba de ejecución uso polling.

3.3.2.3 Waiting


El uso de esta técnica de reconocimiento de finalización de invocación asincrónica de un método requiere del uso de la propiedad AsyncWaitHandle [7] de IAsyncResult. Esta propiedad se utiliza para esperar la finalización la invocación asincrónica de un método. En el ejemplo que viene a continuación, demostramos el uso de esta técnica:


Archivo de código fuente Waiting.cs [enlace alternativo]:

En la líneas 8-13 declaramos el método ProcesoLargo que posee la lógica para introducir un retraso de ejecución simulado y retornar una cadena compuesta por valores calculados (thread actual del dominio de aplicación).


En la línea 15 declaramos el delegado Delegado. Así:

delegate string Delegado (int tiempoRetraso, out int threadEjecucion);


Sobre la línea 21 creamos una instancia de este delegado, y encapsulamos el método ProcesoLargo. Iniciamos la invocación asincrónica en la sentencia de la línea 31. Los argumentos de BeginInvoke son los siguientes:
  • 3000: tiempo de retraso que debe simularse en el método ProcesoLargo.
  • threadEjecucion: hilo sobre el que se ejecutó el método ProcesoLargo.
  • null: Instancia null de AsyncCallback.
  • null: ningún contenedor de datos.
Con la expresión:

iar.AsyncWaitHandle.WaitOne();


en la línea 38 esperamos a que finaliza la ejecución del método asincrónico. La sentencia sobre la línea 43 no se ejecuta sino hasta que lo anterior ocurra.

Compilación:


  1. csc /target:exe Waiting.cs

Ejecución assembly:


  1. .\Waiting.exe

Resultado:

Esta sentencia se ejecutó mientras el método `ProcesoLargo` está en ejecución.
Esta sentencia espera a que la señal `WaitHandle` se complete en `WaitOne`.

Valor de retorno del delegado "Tiempo de retraso: 3000" sobre el thread -1248666768

3.3.2.4 Callback


Con soporte en [1], ~un callback es un método que es invocado por la CLR cuando una llamada (invocación) asincrónica ha finalizado.

Archivo de código fuente Callback.cs [enlace alternativo]:

Compilación:


  1. csc /target:exe Callback.cs

Ejecución assembly:


  1. .\Callback.exe

Resultado:
Prueba ejecución Callback.exe
Figura 2. Prueba ejecución Callback.exe.

4. Práctica: Código Fuente C#

En el siguiente ejemplo aúno los 4 métodos que aprendimos en la sección anterior con un nuevo delegado, un método utilitario para visualización de resultados.

Compilación:


  1. csc /target:exe TecnicasInvocacionAsincronica.cs

Ejecución assembly:


  1. .\TecnicasInvocacionAsincronica.exe

Resultado:
Prueba de ejecución de TecnicasInvocacionAsincronica.exe
Figura 3. Prueba de ejecución de TecnicasInvocacionAsincronica.exe.

5. Conclusiones

El trabajo desarrollado en esta receta demuestra varios de los conceptos de la programación de asincrónica. En particular, quedo demostrado el uso de diferentes técnicas de determinación de completitud de un método asincrónico. La diversidad de técnicas se debe a diferentes grados de control sobre la finalización de la ejecución de un método asincrónico, y esto, es de especial cuidado para situaciones particulares que requieren de la aplicación de una o varias de estas técnicas para resolver el problema en cuestión.

6. Glosario

  • .NET Thread Pool
  • AppDomain
  • Asincrónico
  • BeginInvoke
  • CLR
  • Dominio de aplicación
  • EndInvoke
  • Invocación
  • Multithread
  • Llamada
  • Sincrónico
  • Thread

7. Literatura & Enlaces

[1]: Visual C# 2010 Recipes by Allen Jones and Adam Freeman. Copyright 2010 Allen Jones and Adam Freeman, 978-1-4302-2525-6.
[2]: How to call a Visual C# method asynchronously - http://support.microsoft.com/kb/315582
[3]: Basic Instincts: Asynchronous Method Execution Using Delegates - http://msdn.microsoft.com/en-us/magazine/cc164036.aspx
[4]: Delegados en C#: Introducción | OrtizOL - Experiencias Construcción Software (xCSw) - http://ortizol.blogspot.com/2014/05/Delegados-en-csharp-parte-1-introduccion.html
[5]: AsyncCallback Delegate (System) - http://msdn.microsoft.com/en-us/library/system.asynccallback(v=vs.110).aspx
[6]: IAsyncResult.IsCompleted Property (System) - http://msdn.microsoft.com/en-us/library/system.iasyncresult.iscompleted(v=vs.110).aspx
[7]: IAsyncResult.AsyncWaitHandle Property (System) - http://msdn.microsoft.com/en-us/library/system.iasyncresult.asyncwaithandle(v=vs.110).aspx


M

No hay comentarios:

Publicar un comentario

Envíe sus comentarios, dudas, sugerencias, críticas. Gracias.