Blog dedicado a la programación .NET y la informática en general

Archivo para mayo, 2014

Thread, BackgroundWorker y Task… implementación de métodos asíncronos

En este artículo me voy a centrar en la implementación de los diferentes métodos que existen en .NET para invocar funciones de forma asíncrona, pudiendo informar del progreso del mismo y también ofreciendo una forma de poder cancelar el mismo proceso.

Al final del artículo adjuntaré la solución para Visual Studio con el código del artículo que podréis descargar desde OneDrive.

Cuando comienzo la programación de un método tengo la mala costumbre de no pensar si dicho método puede ser más útil si permite su ejecución de forma asíncrona y esto en ocasiones me cuesta que el proceso global sea más pesado o que incluso la aplicación se quede en modo de espera más tiempo del que debiera no pudiendo ni informar ni cancelar la ejecución. Por esta razón me he decidido a realizar este artículo, para comprender mejor el funcionamiento de los procesos asíncronos y también para iniciarme en la ejecución de métodos de forma paralela, ya que hoy en día con tantos núcleos por procesador podemos reducir considerablemente el tiempo de ejecución de un método síncrono.

No me voy a meter en contar la historia de los procesos asíncronos, para ello os invito a que busquéis en internet, yo iré más al grano, a la implementación. Probablemente si estáis leyendo este artículo será porque desconocéis algunas de las formas que existen para realizar métodos asíncronos. Es relativamente fácil llamar a un método asíncrono que no te devuelva nada o del que no necesitas información, pero en el momento de tener que pasarle parámetros de control la cosa cambia.

Al grano

Actualmente y por razones históricas existen 3 formas de realizar llamadas asíncronas que nos permiten ejecutar el código en hilos de ejecución distintos y por tanto poder ser procesadas de forma paralela.

Los métodos de implementación los dividiré por sus clases en .NET, más concretamente en la versión de Framework 4.5.

El primero históricamente es la clase Thread disponible desde la versión 1.0 de Framework. De su encapsulación y mejora apareció para el Framewok 2.0 la clase BackgroundWorker donde se intenta mejorar el desarrollo de la ejecución de operaciones que lleven un tiempo considerable encapsulando en esta su invocación, información del progreso y posibilidad de cancelación del proceso asíncrono. Y por último, y ante la avalancha de núcleos por procesador y el potencial que se nos brindaba con la programación paralela, surgió la clase Task desde la versión 4.0 de Framework, aunque en la versión 4.5 se ha potenciado considerablemente, y por tanto será en la que me centre.

La clase Thread

Para invocar a un método de forma asíncrona deberemos tener en cuenta si a este se le ha de pasar parámetros y si de ha de devolver resultados. Me centraré en lo más práctico y amplio del concepto, la llamada asíncrona pasándole parámetros y esperando un resultado.

Thread – Invocación asíncrona

// Lanzamiento del hilo de ejecución asíncrono
Thread hilo = new Thread(tp.ConvertirNumero);
hilo.Start(5);

El método “ConvertirNumero” nos devolverá un número entero en forma de texto y a la vez ralentizará la conversión para poder comprobar los resultados.

El método síncrono para poder ser llamado de forma síncrona por “Thread” debe cumplir ciertos requisitos. Por ejemplo devolver “void” y que los parámetros estén encapsulados en un único objeto global “Object”. Os preguntaréis, si tiene que devolver “void” ¿cómo voy a obtener el resultado de la ejecución?, lo vemos en el siguiente apartado, tranquilos.

Thread – Control de progreso y resultado

La única forma de saber qué está haciendo el proceso asíncrono es mediante delegados y eventos y devolviendo una clase con los valores que creamos necesarios, como por ejemplo el progreso de la operación o el resultado del mismo.

Por tanto en el ejemplo para evitar tener que declarar un delegado personalizado he usado “EventHandler” y me he creado una clase personalizada que hereda de “EventArgs” con el progreso y el resultado del proceso que rescataré más adelante.

/// <summary>
/// Evento para controlar el progreso y resultado del proceso asíncrono con Thread
/// </summary>
public event EventHandler Procesando;

//...

public class ProgresoEventArgs : EventArgs
{
public string MensajeProgreso { get; set; }
public string Resultado { get; set; }
}

Durante la ejecución o al final de ella podremos informar del progreso comprobando previamente si el evento ha sido asociado a un gestor de eventos por código, tal y como muestro acontinuación.

// Una vez finalizado usa el mismo evento de información de progreso para indicarlo
if (Procesando != null)
{
ProgresoEventArgs ea = new ProgresoEventArgs();
ea.MensajeProgreso = ObtenerProgreso(x, cuenta);
ea.Resultado = x.ToString();
Procesando(this, ea);
}

Thread – Cancelación de proceso asíncrono

Para poder cancelar el proceso necesitamos hacer uso de una variable que sea accesible desde cualquier hilo de ejecución para poder ser modificada y vigilada. Para ello anteriormente se usaba una variable que debía ser manejada con la instrucción “lock”, que esperaba a que la variable fuese liberada en otro hilo de ejecución antes de ser comprobada o modificada en el actual. Ahora no hace falta esa implementación gracias al tipo “volatile”.

/// <summary>
/// Para la ejecución asíncrona con Threads se necesita una variable que indique si es necesaria la detención del método
/// y que sea invocable de forma pública
/// </summary>
private volatile bool detener;

Dicha variable será vigilada durante el proceso asíncrono para detenerlo cuando cambie su estado. En mi ejemplo se cambia gracias a un método público de la clase.

while (cuenta < x)
{
//...

// Comprueba si ha de detenerse el proceso
if (detener)
{
break;
}
}

Thread – Proceso asíncrono

El código del proceso asíncrono, ya uniendo el control de cancelación, progreso y resultado quedaría de la siguiente forma:

/// <summary>
/// Método para usarlo con Threads
/// </summary>
/// <param name="xObj">Necesita que el parámetro de entrada sea una clase genérica Object para poder pasarle cualquier clase con los parámetros necesarios</param>
public void ConvertirNumero(object xObj)
{
// Inicialiaza la variable que indica si ha de detenerse el proceso asíncrono
detener = false;

int x = (int)xObj;
int cuenta = 0;
while (cuenta < x)
{
Thread.Sleep(500);
cuenta++;

// Comprueba si el evento que indica el proceso está siendo manejado o no
if (Procesando != null)
{
// Indica el progreso
ProgresoEventArgs ea = new ProgresoEventArgs();
ea.MensajeProgreso = ObtenerProgreso(x, cuenta);
Procesando(this, ea);
}

// Comprueba si ha de detenerse el proceso
if (detener)
{
break;
}
}

// Una vez finalizado usa el mismo evento de información de progreso para indicarlo
if (Procesando != null)
{
ProgresoEventArgs ea = new ProgresoEventArgs();
ea.MensajeProgreso = ObtenerProgreso(x, cuenta);
ea.Resultado = x.ToString();
Procesando(this, ea);
}
}

La clase BackgroundWorker

Esta clase ya viene preparada para poder pasarle parámetros, recoger el resultado de la operación y la gestión de eventos de progreso, cancelación y finalización.

BackgroundWorker – Construcción del objeto

Para los que seguimos usando los formularios Windows y nos gusta usar las barras de progreso esta clase está disponible como un control en la barra de herramientas que puedes arrastrar hasta tu formulario.

Captura

Lo primero que hay que hacer cuando se construye el objeto es indicarle si vamos a manejar el progreso y la cancelación del método asíncrono y por tanto asociarle los eventos de control.

// Inicialización del BackgroundWorker
bw.WorkerReportsProgress = true;
bw.WorkerSupportsCancellation = true;
bw.DoWork += bw_DoWork;
bw.ProgressChanged += bw_ProgressChanged;
bw.RunWorkerCompleted += bw_RunWorkerCompleted;

Como podréis comprobar a diferencia de “Thread” la clase ya tiene implementada la metodología necesaria.

BackgrounWorker – Gestores de Eventos

La invocación al método asíncrono sería de la siguiente forma:

private void btnAsincrono_Click(object sender, EventArgs e)
{
this.btnCancelar.Enabled = true;
this.btnAsincrono.Enabled = false;
// Este método se encarga de invocar al evento asíncrono DoWork
bw.RunWorkerAsync(new object[] { 5 });
}

Y dentro del gestor del evento “DoWork” es cuando se llama al método síncrono que queremos controlar. Para ello también debe cumplir ciertos requisitos, como pasarle el objeto “BackgroundWorker” que lo está controlando para poder informar del progreso, y también el objeto de la clase “CancelEventArgs” para vigilar si el proceso ha de finalizarse por petición del usuario.

void bw_DoWork(object sender, DoWorkEventArgs e)
{
// Evento asíncrono que llama al proceso síncrono
int valor = (int)((object[])e.Argument)[0];
BackgroundWorker worker = (BackgroundWorker)sender;
e.Result = new TareaPesada().ConvertirNumero(valor, worker, e);
}

A diferencia de Thread esta vez sí que se permite la devolución de un resultado de la ejecución asíncrona, que podremos rescatar en el evento de finalización.

void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// Aquí se puede rescatar el resultado de la operación con e.Result
this.btnAsincrono.Enabled = true;
this.btnCancelar.Enabled = false;
}

Y  para mostrar el progreso del método asíncrono bastará con recuperar el parámetro que nos pasa el evento en forma de “EventArgs”

void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.lblProgreso.Text = (string)e.UserState;
}

BackgrounWorker – Cancelación del proceso y proceso asíncrono

Cancelar el proceso es igual de fácil, tan sólo hay que invocar al método implementado.

private void btnCancelar_Click(object sender, EventArgs e)
{
// Invoca al método que detiene el proceso asíncrono
bw.CancelAsync();
this.btnCancelar.Enabled = false;
this.btnAsincrono.Enabled = true;
}

Y para finalizar el ejemplo el código que controla el proceso asíncrono:

public string ConvertirNumero(int x, BackgroundWorker bw, CancelEventArgs e)
{
detener = false;
int cuenta = 0;
while (cuenta < x)
{
Thread.Sleep(500);
cuenta++;
if (!bw.CancellationPending)
{
bw.ReportProgress((int)(cuenta * 100 / x), ObtenerProgreso(x, cuenta));
}
else
{
// Cancela el hilo de ejecución
e.Cancel = true;
break;
}
}
bw.ReportProgress((int)(cuenta * 100 / x), ObtenerProgreso(x, cuenta));

return x.ToString();
}

La clase Task

Para tareas más complejas y sobre todo para la programación paralela el uso de Task es la recomendación de Microsoft y por el momento la más actual.

Task – Implementación

Nos permite encapsular en la misma clase un método síncrono como asíncrono mediante el uso de las instrucciones “async” y “await”.

Si queremos controlar el progreso y la cancelación tendremos que pasárselo como objetos en los parámetros de entrada al método.

public string ConvertirNumero(int x, IProgress<string> progress, CancellationToken ct)
{
int cuenta = 0;
while (cuenta < x)
{
if (progress != null)
{
progress.Report(ObtenerProgreso(x, cuenta));
}

Thread.Sleep(500);
cuenta++;
// Genera una excepción que prefiero no tener que controlar
//ct.ThrowIfCancellationRequested();
if (ct.IsCancellationRequested)
{
break;
}
}
if (progress != null)
{
progress.Report(ObtenerProgreso(x, cuenta));
}
return x.ToString();
}

public async Task<string> ConvertirNumeroProgresoCancelableAsync(int x, IProgress<string> progress, CancellationToken ct)
{
return await Task.Run(() => { return ConvertirNumero(x, progress, ct); });
}

Un método se convierte automáticamente en asíncrono si en su declaración le ponemos delante la instrucción “async”, pero OJO porque dentro de la misma estará esperando a una instrucción “await”.

Task – Llamada asíncrona

Para consumir el método asíncrono, en el ejemplo, también se va a realizar desde otro método asíncrono donde se manipulen los controles de la interfaz de usuario. El concepto cambia, ya que desde el mismo evento del control las tareas que se hagan son paralelas a lo que pase en la interfaz de usuario, ya no hay que incluir unos gestores de eventos para monitorizar los eventos, sino que que hay una clase para cada uso, una clase “Progress” con un delegado para manejar el progreso y otra clase “CancellationTokenSource” para solicitar la cancelación del proceso asíncrono, por lo que habrá que guardar su objeto a nivel global de la clase para poder invocar la cancelación en cualquier otro momento.

#region Variables
CancellationTokenSource cts;
#endregion
//...
private async void btnAsincrono_Click(object sender, EventArgs e)
{
// Prepara los controles de la interfaz de usuaro
btnAsincrono.Enabled = false;
btnCancelar.Enabled = true;

// Prepara la llamada al método asíncrono y los objetos de control del mismo
TareaPesada tar = new TareaPesada();
Progress<string> progreso = new Progress<string>(MostrarProgreso);
cts = new CancellationTokenSource();
string res = await tar.ConvertirNumeroProgresoCancelableAsync(10, progreso, cts.Token);

// Devuelve al estado original a los controls de la interfaz de usuario
btnCancelar.Enabled = false;
btnAsincrono.Enabled = true;
}
private void btnCancelar_Click(object sender, EventArgs e)
{
if (cts != null)
{
cts.Cancel();
}
}
private void MostrarProgreso(string progresoTxt)
{
this.lblProgreso.Text = progresoTxt;
}

Resumen

Thread nos permite hacer invocaciones asíncronas, los inconvenientes principales son, el paso de parámetros no está tipado, para controlar el progreso y resultado se de debe hacer una clase especial (tipo EventArgs) para poder obtener los resultados mediante delegados, ha de usarse también variables de tipo «volatil» para poder controlar el estado y en general es algo complejo.

BackgrounWorker tiene un gran inconveniente, y es si quieres que la clase contenga un método síncrono y otro asíncrono, por ejemplo «ConvertirNumero» y «ConvertirNumeroAsync» su implementación se hace bastante tediosa. Además se hace compleja de manejar en procesos paralelos que necesiten cierto nivel de control sobre el mismo.

Task, como ya dije, es el método recomendado por Microsoft y el que ahora mismo se adapta mejor a las necesidades de los desarrolladores, además de ahorrarnos una buena parte de líneas de código.

Descarga de código fuente

Anuncio publicitario
A %d blogueros les gusta esto: