22 de Diciembre de 2014
Ago
26

.NET Tutorial 51. Aplicaciones con hilos. Uso práctico de BackgroundWorker

Categorías: 

No hace mucho Btc me comentó algunos problemas que tenía con su B-File Renamer, en concreto con la gestión de hilos que hace su programa para el renombrado masivo de los archivos. Por este motivo me he animado a escribir este pequeño tutorial para mostrar un pequeño ejemplo práctico de como implementar una 'solución' que emplea el uso de hilos (threading en inglés).

Una de las principales ventajas de las aplicaciones que emplean hilos es que pueden hacer 'más de una cosa' al mismo tiempo. En la realidad prácticamente todas las aplicaciones ya usan internamente más de un hilo, lo que ocurre es que esto se realiza de forma transparente para nosotros.

En nuestro caso queremos que un determinado método, función, clase o lo que sea se ejecute en otro hilo. Normalmente estos métodos, funciones, clases o lo que sea que queremos ejecutar dentro de un hilo consumen bastante tiempo.

Pongamos el caso del B-File Renamer. Renombrar todos los archivos de una determinada carpeta puede llevar 1 minuto o más, dependiendo lógicamente de la cantidad de archivos a renombrar.

Lo que no debería hacer la aplicación (B-File Renamer en este caso) es 'quedarse colgada' mientras renombra 2000 ficheros.

En este tipo de escenarios se puede implementar un BackgroundWorker, que es lo que veremos a continuación.

Para ello he simulado una aplicación que calcula la suma de todos los números comprendidos entre 1 y 1000

A esta suma se le ha añadido un retardo (Sleep) para 'simular' la carga de trabajo del método en cuestión.

La aplicación básicamente es esta:

Se calculará la suma sin usar hilos y usando un BackgroundWorker

Además se dispone de una barra de progreso que indica cuanto falta para finalizar todo el proceso y un botón de 'stop' que detiene el proceso.

Si se detiene el proceso se indica en la etiqueta correspondiente con el texto "Cancelado"

Si ejecutáis el programa, aparentemente tanto la opción ejecutar sin 'Hilos' como la opción usar 'BackgroundWorker' funcionan igual: muestran el resultado, la barra de progreso se va actualizando, el botón 'stop' funciona... sin embargo esto es solo apariencia. :)

Si ejecutáis el ejemplo sin usar hilos, intentar mover la ventana mientras se está ejecutando. Oh sorpresa! Observaréis que tanto el resultado como la barra de progreso 'se han parado'.

Cuando dejéis de mover la ventana observaréis que tanto el resultado como la barra de progreso se vuelven a actualizar.

Pero hay más, si ahora pulsáis sobre el menú Archivo, al seleccionar cualquier elemento del menú se muestra un MessageBox. Al mostrarse el MessageBox se puede observar que tanto el resultado como la barra de progreso 'se han parado'

Nada de esto ocurre cuando se usa un BackgroundWorker. Podéis mover la ventana de la aplicación o pulsar en cualquier item del menú y veréis que tanto el resultado como la barra de progreso se siguen 'actualizando'

Del BackgroundWorker nos serán útiles 3 eventos:

  • DoWork
  • ProgressChanged
  • RunWorkerCompleted

DoWork

El evento DoWork se dispara cuando se llama al método RunWorkerAsync

En nuestro caso tendremos algo como esto:

Private Sub BackgroundWorker1_DoWork()
   e.Result = DoAlgo()
End Sub

DoAlgo() es la función que calcula la suma de los 1000 primeros números. Esta función devuelve el resultado en un dato de tipo Integer que se asigna a e.Result que es de tipo genérico (Object)

Cuando DoAlgo() haya calculado la suma de los 1000 primeros números, guardará el resultado de dicha suma en e.Result y se disparará inmediatamente el envento  RunWorkerCompleted()

Para evitar que se dispare el evento DoWork antes de que el BackgroundWorker haya finalizado la tarea se puede llamar a la función IsBusy:

If Not BackgroundWorker1.IsBusy Then
   BackgroundWorker1.RunWorkerAsync()
End If

ProgessChanged

Este evento se dispara cuando se llama al método ReportProgress

Dentro de nuestra función DoAlgo() vamos sumando los números. Al tratarse de un simple bucle For se puede computar fácilmente el porcentaje de números que llevamos sumados:

valorPorcentaje = Convert.ToInt32((i / Total) * 100)
BackgroundWorker1.ReportProgress(valorPorcentaje)

Dentro del evento ProgressChanged tenemos:

Private Sub BackgroundWorker1_ProgressChanged()
   Me.ProgressBar1.Value = e.ProgressPercentage
End Sub

Como se puede ver, lo único que se hace es actualizar el valor un control ProgresBar

Aquí cabe destacar que en este caso no es necesario utilizar ningún delegado para actualizar el contenido del control Progressbar.

Si intentáis cambiar el texto, la visibilidad, enable/disable, etc de un control (botón, etiqueta, caja de texto, listview, etc) desde dentro de un hilo distinto al hilo principal, deberéis usar delegados.

En nuestro ejemplo se muestra el uso de un delegado en la función MostrarResultado() que actualiza el texto de la etiqueta LblStatus desde dentro de DoAlgo() . DoAlgo() se está ejecutando en un hilo distintoal hilo principal del programa.

Otra opción es usar por ejemplo en el Form_Load() la siguiente instrucción: (MSDN)

Control.CheckForIllegalCrossThreadCalls = False  

Con eso podríamos usar LblStatus.Text = retSuma.ToString() desde dentro de DoAlgo() sin que se produjese ninguna excepción.

RunWorkerCompleted

Este evento se dispara cuando el método, función, etc que se ha iniciado en DoWork ha finalizado. También se dispara este evento cuando se cancela el proceso.

Para cancelar el proceso se llama al método CancelAsync

En nuestro caso tenemos un botón "stop" que hace lo siguiente:

BackgroundWorker1.CancelAsync()

Luego, dentro de nuestro DoAlgo() tenemos algo como esto:

For xxxx
   retSuma = realizar suma

   If BackgroundWorker1.CancellationPending Then
      retSuma = Nothing
      Exit For
   End If

Next

Return retSuma

Esto provoca que salga del bucle For y que el resultado que se guarda en e.Result sea Nothing.

Finalmente en el evento RunWorkerCompleted hacemos lo siguiente:

Private Sub BackgroundWorker1_RunWorkerCompleted()
   If e.Result <> Nothing Then
      LblStatus.Text = e.Result
   Else
      LblStatus.Text = "Cancelado"
   End If
End Sub

 

Pues poco más hay que añadir. Cómo habréis visto el uso de un BackgroundWorker es bastante sencillo.

En .NET Framework 4.0 se ha introducido un nuevo concepto para el manejo de 'hilos'. Es lo que se conoce como TPL: Task Parallel Library (MSDN)
La TPL es un concepto interesantísimo al que seguramente dedicaremos alguna entrada.

 

Saludos.
mov eax,ollydbg; Int 13h

Descargar código fuente del .NET Tutorial 51
(23 KB. Visual Studio 2008)

 

5
Valoración media: 5 (1 voto)

1 Comentario:

Disculpen, ¿saben donde

Disculpen, ¿saben donde esta por aqui el departamento de quejas? Tengo que poner 10 estrellas y en mi pantalla solo veo 5!!

Impresionante entrada, lo unico que me tiene un poco liado, es el objeto "e", ya que cambia según el invocador. Por ejemplo, si hablamos de un keypress en un textbox, "e" transporta el caracter o la tecla que se pulsa. Tengo que echarle un buen vistazo a eso.

Saludos!