Tutorial basico para el uso de hilos de ejecucion en aplicaciones VB (Multithreaded applications)

Iniciado por Dogo, 13 de Enero de 2011, 21:07:50 PM

Tema anterior - Siguiente tema

Dogo

Bueno. Ya que no muchos se meten en el area de tutoriales para hacer uno voy a poner algo que me llamo la atencion y que para algunos principiantes en la programacion en Visual Basic puede ser realmente un dolor de cabeza sobre todo por la poca documentacion para principiantes que existe en la red (MSDN no es lo que se podria decir "claro" con respecto a los principiantes).

Actualmente los hilos de ejecucion (Threads) en un entorno de desarrollo se estan volviendo cada vez mas necesarios. En Visual Studio (entiendase como el conjunto de lenguajes que este alberga) estos son bastante simple de crear y controlar. Si bien son bastante faciles de implementar estos tienen algunas limitaciones que los usuarios principiantes pueden obviar y que causarian varios dolores de cabeza.

Normalmente cuando ejecutamos el programa y este tiene funciones que toman demaciado tiempo como puede ser hacer un ping a otro equipo, crear un archivo grande o hacer una descarga desde la web (realmente cualquier funcion que demore mas de 2 segundos ya es mucho), la interface principal del programa se nos queda congelada hasta que la funcion termine y esto puede ser realmente molesto (ademas para lo usuarios finales puede indicar que la aplicacion esta fallando). En estos casos es bueno tener la interfaz y las funciones en hilos de ejecucion aparte para que no se bloqueen entre si. Lo ideal seria que fuesen 2 hilos (El primero para la interfaz y los procesos basicos que son rapidos, y el segundo para procesos largos y lentos).

Primero para comenzar necesitamos alguna version de Visual Studio que se ajuste a nuestras necesidades, en este caso usaremos la version 2008, si bien se dice que puede funcionar con cualquier version yo solo he comprobado su uso en la version 2008 y 2010 por lo que desconosco si versiones antiguas como la 2005 o 2003 puedan servir.

Comenzaremos abriendo el Visual Studio y crearemos una solucion Visual Basic nueva para poder trabajar. Lo ideal seria que ya tuviesemos algun codigo ya escrito para poder modificarlo pero si no existe tendremos que improvisar para hacer las pruebas.

Luego agregaremos algunos componentes al form que tenemos. Agregaremos:
- 1 Boton
- 1 ProgressBar con su propiedad maximum puesta en 100 y su propiedad step en 1
- 1 TextBox con la propiedad Multilinea activada.



Luego lo ordenamos en la pantalla de la forma que queramos.

Continuamos agregando alguna funcion que nos tome bastante tiempo. En este caso crearemos una funcion basica que haga ping 1 vez hacia los DNS de google y que luego llamaremos atravez de un buble FOR. Antes eso si, debemos definir alguna variable global para el intercambio de datos.
[spoiler]
La funcion en si solo es esto:
[spoiler]
     
        res = eco.Send(IP)
        If res.Status = NetworkInformation.IPStatus.Success Then
            Return "OK"
        Else
            Return "No"
        End If
[/spoiler]

Quedandonos todo el codigo asi hasta el momento.
[spoiler]
Imports System.Net ' Importamos las librerias de Red para hacer uso de sus funciones
Public Class Form1
    Dim eco As New System.Net.NetworkInformation.Ping
    Dim res As System.Net.NetworkInformation.PingReply
    Dim ipsx As IPAddress
    Dim Resultado As String
    Dim i As Integer

    Function PingDNS(ByVal IP As IPAddress)
        res = eco.Send(IP) ' Recibimos la IP y hacemos Ping
        If res.Status = NetworkInformation.IPStatus.Success Then ' Comprobamos si el ping funciono y retornamos la respuesta segun el caso
            Return "OK"
        Else
            Return "No"
        End If
    End Function

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click ' Orden al presionar el boton
        ipsx = IPAddress.Parse("8.8.8.8") ' esta es la IP a la cual haremos Ping
        For Me.i = 1 To 100 ' Un ciclo FOR para repetir la funcion de Ping varias veces
            Resultado = PingDNS(ipsx) ' Pasamos la IP a la funcion PingDNS
            TextBox1.Text = TextBox1.Text & vbCrLf & "Respuesta " & i & ": " & Resultado 'Aqui se va mostrando el progreso en el textbox
            ProgressBar1.PerformStep() ' hacemos que la barra de progreso de un paso para mostrar avance
        Next
    End Sub
End Class
[/spoiler]
[/spoiler]

Hasta el momento si ejecutamos el codigo y presionamos el boton veremos como la interface principal se nos queda congelada mientras la funcion este en ejecucion volviendo a la normalidad una vez que esta termina. Hasta aqui todo el programa lleva la ejecucion sobre un solo hilo. Ahora, para crear mas hilos solo debemos agregar el componente BackgroundWorker y modificar un poco el codigo.

El BackGroundWorker lo podemos agregar de 2 formas, agregandolo directamente desde la seleccion de componentes en el IDE (del mismo lugar de donde salio el boton y el textbox), o atravez de codigo que si bien es la forma que me gusta a mi es algo mas complicado para los principiantes ya que hay que definir sus propiedades desde el codigo tambien. En este caso lo vamos a hacer desde el IDE para no hacer tan espeso el tema.

En este caso solo vamos al cuadro de herramientas y elegimos bajo la seccion componentes el BackGroundWorker y lo arrastramos a la ventana principal y cambiamos sus propiedades para que queden igual que en la imagen.







Ahora viene la parte buena, debemos mover el codigo hacia el evento DoWork del BackgroundWorker (el evento se crea automaticamente haciendo doble clic sobre el BackgroundWorker). Solo moveremos la parte que nos ejecuta nuestra funcion larga, es decir la parte que ejecuta nuestro ping y que se encuentra en el codigo del boton. Hay que tener en cuenta que los BackgroundWorker no pueden acceder a los elementos de la GUI de forma directa por lo que si nos fijamos en el codigo anterior estariamos en problemas ya que estamos interactuando con la ProgressBar y el TextBox. Por suerte para nosotros existe el evento ProgressChanged que nos permitira interactuar con la GUI a medida que reportamos nuevos avances en el trabajo del BackgroundWorker.

Para crear el evento ProgressChanged tan solo debemos seleccionar el BackgoundWorker1 desde el menu superior izquierdo y luego seleccionamos desde la lista superior derecha el nuevo evento ProgressChanged, esto nos creara al final de nuestro codigo un nuevo fragmento en el cual podemos agregar nuestro codigo para interactuar con la GUI.



[spoiler]
El nuevo evento DoWork del BackgroundWorker
[spoiler]    Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        ' Lo de abajo es el fragmento que gatillaba la funcion Ping
        ipsx = IPAddress.Parse("8.8.8.8") ' esta es la IP a la cual haremos Ping
        For Me.i = 1 To 100 ' Un ciclo FOR para repetir la funcion de Ping varias veces, 100 en este caso
            Resultado = PingDNS(ipsx) ' Pasamos la IP a la funcion PingDNS
            BackgroundWorker1.ReportProgress(i)  ' Aqui reportamos el progreso que hacemos, en este caso el progreso es el numero de ciclo FOR
        Next
    End Sub
[/spoiler]

y el evento ProgressChanged
[spoiler]    Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
        ' Lo de abajo es el trozo que estaba en el boton e interactuaba con la GUI
        ProgressBar1.PerformStep() ' hacemos que la barra de progreso de un paso para mostrar avance
        TextBox1.Text = TextBox1.Text & vbCrLf & "Respuesta " & i & ": " & Resultado 'Aqui se va mostrando el progreso en el textbox
    End Sub
[/spoiler]

El codigo completo hasta ahora.

[spoiler]Imports System.Net ' Importamos las librerias de Red para hacer uso de sus funciones
Public Class Form1
    Dim eco As New System.Net.NetworkInformation.Ping
    Dim res As System.Net.NetworkInformation.PingReply
    Dim ipsx As IPAddress
    Dim Resultado As String
    Dim i As Integer

    Function PingDNS(ByVal IP As IPAddress)
        res = eco.Send(IP) ' Recibimos la IP y hacemos Ping
        If res.Status = NetworkInformation.IPStatus.Success Then ' Comprobamos si el ping funciono y retornamos la respuesta segun el caso
            Return "OK"
        Else
            Return "No"
        End If
    End Function

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click ' Orden al presionar el boton

    End Sub

    Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        ' Lo de abajo es el fragmento que gatillaba la funcion Ping
        ipsx = IPAddress.Parse("8.8.8.8") ' esta es la IP a la cual haremos Ping
        For Me.i = 1 To 100 ' Un ciclo FOR para repetir la funcion de Ping varias veces, 100 en este caso
            Resultado = PingDNS(ipsx) ' Pasamos la IP a la funcion PingDNS
            BackgroundWorker1.ReportProgress(i)  ' Aqui reportamos el progreso que hacemos, en este caso el progreso es el numero de ciclo FOR
        Next
    End Sub

    Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
        ' Lo de abajo es el trozo que estaba en el boton e interactuaba con la GUI
        ProgressBar1.PerformStep() ' hacemos que la barra de progreso de un paso para mostrar avance
        TextBox1.Text = TextBox1.Text & vbCrLf & "Respuesta " & i & ": " & Resultado 'Aqui se va mostrando el progreso en el textbox
    End Sub
End Class
[/spoiler]
[/spoiler]

Hasta ahora si ejecutamos el codigo y presionamos el boton no va a pasar absolutamente nada por que aun no se ha dado la orden para que se active el segundo hilo de ejecucion. Para hacer esto tan solo debemos agregar la instruccion BackgroundWorker1.RunWorkerAsync() bajo el codigo del boton que actualmente no tiene nada ya que movimos el codigo a otras partes.

   Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click ' Orden al presionar el boton
        BackgroundWorker1.RunWorkerAsync()
    End Sub

Ahora al ejecutar el codigo y presionar el boton la GUI del programa deberia funcionar como siempre e incluso ir mostrando el progreso atravez del textbox y la progressbar.

Algunas consideraciones a tener en cuenta
- Los BackgroundWorkers no pueden acceder a elementos de la GUI por el simple hecho que esta se encuentra en un hilo aparte y tratar de hacerlo nos daria una advertencia de seguridad y nos botaria el programa. De todas formas estos si pueden acceder a variables globales e incluso modificarlas a pesar de que no sea muy recomendado.
- Si un BackgroundWorker no tiene la propiedad WorkerReportProgress en True el evento ProgressChanged no se activara y el worker hara su trabajo sin avisar nada. Esto es util en algunos casos especificos pero no se recomienda mucho.
- Bajo ningun motivo se puede ejecutar la instruccion BackgroundWorker1.RunWorkerAsync() dentro de algun bucle ya que una vez iniciado el hilo este permanecera ocupado hasta terminar o hasta que sea cancelado. En caso de meter la instruccion dentro de un bucle el worker funcionara el primer ciclo pero en el segundo nos dara un error y si no lo tenemos controlado nos hara caer la aplicacion (esto lo pueden comprobar presionando 2 veces el boton en este programa de muestra).
- Buena parte de los eventos necesarios para cualquier programa se crean de forma automatica haciendo doble click sobre el componente. En caso de no crearse siempre se puede hacer desde los menues superiores de la vista de codigo.

Link codigo fuente
MMmmmm firma.... algun dia