Colabora
 

Desarrollo de aplicaciones multihilo con Visual Studio 2005

El componente BackgroundWorker

 

Fecha: 02/Ene/2010 (09-12-2009)
Autor: José Alejandro Lugo García - [email protected]

 


Introducción

Trabajar con hilos es requisito casi explícito de toda aplicación que se de a respetar en el mundo actual del desarrollo de aplicaciones informáticas. La programación multihilo ofrece grandes ventajas en comparación con aquella donde no se hace uso de estas bondades. Las aplicaciones de escritorio por ejemplo disponen de un proceso asignado para la renderización de su interfaz gráfica. Si nos disponemos a realizar una tarea pesada al dar clic a un botón, es de esperar que los gráficos desaparezcan pues se ha procedido a realizar ahora un proceso que consume los recursos de aquel que se encargaba de dibujar la interfaz.

El presente artículo describe las ventajas que trae incluidas el IDE Visual Studio 2005 para el trabajo con hilos mediante el uso de un nuevo componente: el backgroundWorker. Explica conceptos previos y muestra la importancia de abstraer funcionalidades del trabajo multihilo a artefactos como este. Para los ejemplos resueltos se utiliza el lenguaje C# 2.0.

Conceptos previos

Adentrándonos más en el tema, me gustaría decir que también pude haberme referido a esto de los hilos como "manejar procesos de manera asíncrona" . Y es que es costumbre de nosotros mencionar palabritas que relacionamos con algo pero no sabemos su verdadero significado. Entendamos lo que quiere decir realizar procesos sincrónica y asincrónicamente.

Los procesos sincrónicos son aquellos que se ejecutan uno detras de otro, o sea, si se tiene una lista de 3 procesos a realizar se comenzará por ejecutar el primero y hasta que el micro no termine su realización pues no pasará al segundo y de igual manera ocurrirá con el tercero.

Los procesos asincrónicos son aquellos que pueden ser ejecutados en paralelo. Para los que han dado la asignatura de Sistemas Operativos, sabrán qué tanto significa la definición de paralelo. Para aquellos que aun no han tenido el privilegio les diré en breves palabras que un microprocesador no puede ejecutar más de una tarea al mismo tiempo. Lo que verdaderamente hace en estos casos es dedicarle instantes a cada tarea por separado hasta llevarlas a su realizacion final. Lo que nos da la sensación de ese "paralelismo" es que dedica milesimas de segundo para cada proceso que se esté ejecutando de manera asíncrona. ¡Vaya caramba! otra vez con la palabrita, disculpen quise decir: que se esté ejecutando a la par del otro. Esto lo puedes notar, si estás escuchando música en el instante mismo en que lees este artículo, viendo que puedes seguir utilizando tu navegador Web para realizar otras busquedas o abrir al mismo tiempo Microsoft Word. No es que el micro lo esté llevando todo a la vez, ¡es que atiende a todas de una manera rápida por separado! y ese es el verdadero concepto de paralelismo aunque OJO: refiriéndonos a un micro. En los ordenadores donde están instalados más de un micro, como el caso de los dual-core, ahí sí que es perfectamente posible que un micro esté trabajando en una tarea y el otro en otra, o sea, dos tareas ejecutadas perfectamente a la vez. Lo que sí no sucede repetimos es el caso de que uno pueda con dos tareas enteramente simultáneas.

Habiendo entendido lo anterior, podemos proseguir explicando otros conceptos necesarios en torno a la programación multihilo. Respecto a esto debemos hacer notar que la misma vive en estrecha relación con el concepto de delegado y evento. Un delegado es:

"un tipo especial de clase cuyos objetos pueden almacenar referencias a uno o más métodos de tal manera que a través del objeto sea posible solicitar la ejecución en cadena de todos ellos."

En otras palabras un delegado puede contener la ejecución de uno o varios métodos. Y es que una de las formas de ejecutar tareas de forma asíncrona es mediante la función BeginInvoke() de un objeto de tipo delegado. La propia clase Thread al pasársele un método a su constructor para ejecutar un subproceso, tiene definido un tipo (el ThreadStart) de tipo delegado.

Un evento a su vez "es una variante de las propiedades para los campos cuyos tipos sean delegados. Es decir, permiten controlar la forman en que se accede a los campos delegados y dan la posibilidad de asociar código a ejecutar cada vez que se añada o elimine un método de un campo delegado."

En todo momento de la ejecución de una tarea es necesario conocer su estado y el valor final devuelto al cabo de la realización de la misma. Esta problemática es manejada por los dos conceptos vistos anteriormente. Los delegados se encargarán de ejecutar tareas asíncronas y los eventos de ir diciendo cómo marcha el estado de las mismas hasta que se concluya con un valor devuelto.

Situación problémica

Tal vez por desconocimiento, muchos usuarios siguen pensando que la clase Thread es la única que nos permite manejar varios procesos a la vez. Si bien ofrece opciones de ejecutar tareas en otro hilo independiente al principal de la aplicación, no contiene a simple vista opciones de mostrar el progreso de una tarea asi como tampoco encapsula toda la lógica de lo que implica "arrancar un hilo". Veamos la siguiente situación problémica que suele ser tan cotidinana en la vida de un desarrollador.

Se desea una aplicación que se conecta con 3 Web Services (WSs) que permitirán descargar nuevos informes. Por razones de trafico en la red conectarse se demora, pero a la vez el jefe quiere seguir trabajando accediendo a reportes del día anterior. El jefe es una persona desesperada y no ver el progreso de la conexión para descargar los nuevos reportes le pone mal por tanto quisiera poder ver el avance de esta conexión para ver si le da tiempo a tomarse un café. En caso de necesitar salir en el momento de conexión y por demorar mucho, también debe de poder cancelar la operación para que nadie vea los informes no estando él delante de su PC.

Cuando un programador lee esto, enseguida le viene a la mente: "Multihilo". Pero uff, el progreso, ese progreso que a todos nos rompe la cabeza y que tan trivial pero repetitivo resulta utilizando eventos y delegados. Somos programadores que necesitamos agilidad en nuestro trabajo. La lógica siempre es la misma. Se desea ejecutar un hilo, conocer el progreso de dicha actividad, poder cancelarla durante su ejecución y una vez finalizada la misma devolver el resultado esperado. ¿Y si surgiera un error cuando se está conectando a los WSs? Por supuesto, que si surge una excepción en medio de la ejecución del hilo, pues también debe ser posible tratarla. Pero caramba, se me quedaba que Microsoft en su MSDN dice que no debo utilizar un bloque try-catch cuando ejecuto un hilo ya que no va acorde con sus principios. ¿Qué hacer?

Esta variante que les comento a continuación me vino de maravillas y es utilizando el componente BackgroundWorker.

El componente BackgroundWorker

"El componente BackgroundWorker le proporciona la capacidad de ejecutar operaciones que llevan mucho tiempo de forma asincrónica ("en segundo plano"), en un subproceso diferente del subproceso principal de la interfaz de usuario de la aplicación." (MSDN Online)

Este componente tiene 3 eventos principales:

  • DoWork(object sender, DoWorkEventArgs e): Código a ejecutar en el hilo independiente. Aquí ponemos el código pesado de nuestra aplicación. Refiriéndonos al caso de estudio, la conexión con los 3 WSs.
  • ProgressChanged(object sender, ProgressChangedEventArgs e): Muestra el progreso informado por el programador al momento de la conexión con cada WS.
  • RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e): En esta parte se recogen los resultados si la operación fue satisfactoria o cualquier error que pudo suceder. En ambos casos se le puede mostrar al usuario si el éxito de la tarea fue satisfactorio o no.

Así mismo se hace necesario poner en marcha el hilo en alguna parte. Esto se hace por lo general, en algún botón donde le diremos al backgroundWorker que se ponga en marcha con el método RunWorkerAsync(). De igual manera podremos cancelar la puesta en marcha del trabajo ejecutando el método CancelAsync().

Vista la panorámica anterior, no esperemos más, manos a la obra para realizar el caso de estudio anterior.

Solucionando el caso de estudio

Primero debemos agregar un componente backgroundWorker a nuestro formulario. Lo segundo será conectarnos al Web Service (los pasos para agregar referencia a un servicio web en nuestra app no es objetivo de este artículo), creando sus respectivos objetos. Para esto, agregue el siguiente código en el evento DoWork:

[System.Diagnostics.DebuggerNonUserCode]
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
     telematicos.Telematicos c = new Telematicos();

     if (bwWorker.CancellationPending)
     {
        e.Cancel = true;
        return;
     }

     bwWorker.ReportProgress(35);

     identificacion.servicios rpt = new servicios();

     if (bwWorker.CancellationPending)
     {
        e.Cancel = true;
        return;
     }

     bwWorker.ReportProgress(65);

     ciudadano.Personas personas = new Personas();

     if (bwWorker.CancellationPending)
     {
        e.Cancel = true;
        return;
     }

     bwWorker.ReportProgress(90);

     //Hacer aquí algo más liviano. Cargar objetos, etc.

     bwWorker.ReportProgress(100);

     e.Result = "Completado!";
}

Vamos a analizar paso a paso el código anterior. La etiqueta que se le pone encima al método permite mantener al debugger alejado de las excepciones que se puedan generar desde aquí. Con esto en tiempo de Debug se pasa directo al RunWorkerCompleted capturándose la excepción con e.Error. Más adelante veremos por qué hacemos esto. El método ReportProgress() permite definir, a consideración del programador, qué por ciento de la tarea ya va siendo realizada cuando se pasa por cada línea de código pesada. Cada vez que se pase por una de estas líneas se le pregunta si en algun momento el usuario ha solicitado la cancelación de la tarea verificando la propiedad CancellationPending activada por el método CancelAsync(), momento en el cual se "rompe" la ejecución de la tarea. Por último, la propiedad Result de tipo object recogerá el posible resultado final de la tarea, que para este caso, se le ha asignado un mensaje de tipo cadena (string).

Lo tercero que debemos hacer es agregar un botón y poner el siguiente código en él:

private void button1_Click(object sender, EventArgs e)
{
   bwWorker.RunWorkerAsync();
}

Esto permitirá iniciar el código puesto para el evento DoWork().

En cuarto lugar, agregar otro botón con el que podamos cancelar la conexión. Para ello, pongamos lo siguiente:

private void btnCancelar_Click(object sender, EventArgs e)
{
   bwWorker.CancelAsync();
}

En quinto lugar. Debemos poder mostrar al usuario el progreso de la conexión. Para esto agreguemos un control progress bar a nuestra app, un label y escribamos en el evento ProgressChanged() del bwWorker el siguiente código:

private void bwWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
   // Este evento solo ocurre cuando se reporta un progreso   
   progressBar1.Value = e.ProgressPercentage;
   this.lbltResultado.Text = string.Format("Completado: {0}%", e.ProgressPercentage.ToString());
}

La propiedad ProgressPercentage toma los valores que vaya recogiendo el método ReportProgress() del evento DoWork.

En sexto lugar. Vayamos al código del evento RunWorkerCompleted y pongamos lo siguiente:

private void bwWorker_RunWorkerCompleted(object sender,RunWorkerCompletedEventArgs e)
{
   //Si en el hilo del _DoWork se produce alguna excepción, se captura aquí.
   if (e.Error != null)
   {
     MessageBox.Show("Lo sentimos, hubo algun error");
   }
   //Si la operación fue cancelada	
   if (e.Cancelled)
   {
     this.lblResultado.Text = "CANCELADO";
   }
   //si todo fue exitoso
   else
   {
     this.lblResultado.Text = (string)e.Result;
   }
}

La property Error contiene valores de la clase Exception por lo que puede obtenerse el tipo especifico de error derivado de la clase Exception utilizando e.Error.GetType(). Solo comentar que si en el evento DoWork, no hubiéramos puesto la etiqueta [System.Diagnostics.DebuggerNonUserCode], en caso de producirse algún error en tiempo de debuggueo de nuestra APP, la línea de traceo no hubiera llegado hasta este evento RunWorkerCompleted sino que se hubiera quedado allá en el DoWork, no dando el mensaje de error que se hubiera esperado normalmente. Puede probar quitando esta etiqueta y haciendo que suceda una excepción para que vea con sus propios ojos a lo que me refiero. Este suceso se encuentra documentado en varios sitios de internet (en algunos incluso como bug de Microsoft) y la solución que se propone es la de la etiqueta mencionada en el evento DoWork. Como decía anteriormente: "Con esto en tiempo de Debug se pasa directo al RunWorkerCompleted capturándose la excepción con e.Error". Si la tarea no produjo ningún error ni fue cancelada se pasa a obtener su resultado final con la property Result. En este caso se obtiene por un label "Resultado" el valor "Completado!".

Conclusiones

El componente BackgroundWorker ofrece grandes ventajas para el manejo de tareas en segundo plano. Encapsula toda una lógica de trabajo, permitiéndole al programador ganar provecho con el ahorro de tiempo que su uso implica. Con él, se logra una abstracción aún mayor sobre la utilización de delegados y los eventos en las tareas de este tipo, pues su propia programación se basa en el uso seguro y apropiado de los mismos.


Compromiso del autor del artículo con el sitio del Guille:

Lo comentado en este artículo está probado (y funciona) con la siguiente configuración:

El autor se compromete personalmente de que lo expuesto en este artículo es cierto y lo ha comprobado usando la configuración indicada anteriormente.

En cualquier caso, el Guille no se responsabiliza del contenido de este artículo.

Si encuentras alguna errata o fallo en algún link (enlace), por favor comunícalo usando este link:

Gracias.



Código de ejemplo (comprimido):

 

Este artículo no tiene ningún código de ejemplo

Por trabajar con Web Services que solo están disponibles dentro de mi entorno laboral, no incluí ningún proyecto con código fuente pues en otro ambiente no correría de manera correcta. No obstante el lector puede utilizar como ejemplos los códigos que aparecen en el artículo, y probarlos con web services u otros procesos de alto consumo de memoria que estén disponibles dentro de su ámbito de trabajo.


Ir al índice principal de el Guille