El blog del burgués

22 febrero 2011

Usando Threads en C#.NET: BackgroundWorker

Filed under: C# — elburgues @ 1:21 AM

Desarrollando aplicaciones muchas veces habremos tenido la necesidad de realizar una operación que potencialmente puede llevar unos cuantos segundos, incluso minutos. El problema que presenta esta situación es que si realizamos dicha operación en el hilo principal de nuestra aplicación, aunque sea por unos pocos segundos, la interfaz de nuestra aplicación deja de responder a nuestros movimientos con el ratón y/o el teclado, dando la sensación de que la aplicación se ha quedado bloqueada (aunque realmente no sea así), provocando incertidumbre e insatisfacción en el usuario. Lo deseable en estos casos sería realizar dicha tarea en un segundo hilo de trabajo e ir obteniendo información acerca del estado de la operación en la interfaz gráfica del programa. Sería deseable también tener la posibilidad de cancelar la tarea en cualquier momento, así como también tener la posibilidad de seguir interactuando con nuestra aplicación hasta que la operación en un segundo plano termine y nos ofrezca los resultados. BackgroundWorker es una clase perteneciente al espacio de nombres System.ComponentModel y sirve para gestionar todo esto de lo que estamos hablando. Es una clase muy interesante, ya que con relativo poco esfuerzo, se logran resultados gratificantes. Ciertamente, debe encapsular bastante trabajo hecho en su interior para nosotros. Entre sus características más reseñables están:

  • Proporciona implementación de la interfaz IComponent, permitiendo ser situado en el diseñador de visual studio.
  • Gestiona excepciones en el hilo de trabajo, lo cual quiere decir que no es necesario incluir bloques try/catch en el hilo de trabajo.
  • Permite actualizar windows forms y controles WPF.

Vemos un ejemplo de su uso. He procurado comentar al máximo el código, explicando cada detalle que pueda resultar de interés:

using System;
using System.ComponentModel;
using System.Threading;

class Program
{
    static BackgroundWorker bw;

    static void Main()
    {
        // Creamos una instancia de la clase BackgroundWorker
        // con el objetivo de configurarla a nuestro gusto
        // para posteriormente arrancar la operación asíncrona.
        bw = new BackgroundWorker();

        // Queremos estar informados del estado de la operación.
        // Esto nos habilita para poder llamar a bw.ReportProgress().
        bw.WorkerReportsProgress = true;

        // Queremos tener la posibilidad de cancelar el proceso.
        // Esto nos habilita para poder llamar a bw.CancelAsync().
        bw.WorkerSupportsCancellation = true;

        // Suscripción a eventos.

        // DoWork es el hilo en donde se va a realizar la operación.
        bw.DoWork += bw_DoWork;

        // Este evento es el que tenemos que aprovechar para ser informados
        // del estado de la operación.
        bw.ProgressChanged += bw_ProgressChanged;

        // Este evento es el que se va a disparar cuando la tarea haya finalizado.
        // Aquí es donde vamos a poder recoger los resultados de la operación.
        bw.RunWorkerCompleted += bw_RunWorkerCompleted;

        // Una vez que ya hemos configurado el backgroundWorker como hemos querido
        // este método sirve para poner en marcha la operación
        // que potencialmente puede llevar un tiempo más o menos grande en realizarse.
        bw.RunWorkerAsync("¡Hola, hilo de trabajo!");

        // Ten en cuenta que después de la anterior llamada,
        // el hilo principal de la aplicación sigue con lo suyo.
        Console.WriteLine("Presiona ENTER dentro de los próximos 5 segundos para cancelar.");

        // Esto nos va a servir para que la aplicación no termine.
        Console.ReadLine();

        if (bw.IsBusy)  // Si estamos metidos de lleno en el bw_DoWork...
            bw.CancelAsync();   // pues le vamos a dar a cancelar.

        // Pulsamos cualquier tecla para finalizar.
        Console.ReadLine();
    }

    ///
    ///
    ///
    /// <param name="sender" />
    /// <param name="e" />
    ///
    /// DoWork se dispara en un hilo diferente al hilo principal de la aplicación.
    /// Desde aqui no podemos hacer referencia a ningún control de la aplicación.
    /// No es necesario que aquí coloquemos bloques try/catch.
    /// Si ocurre una excepción, bw_DoWork finalizará repentinamente
    /// disparándose inmediatamente bw_RunWorkerCompleted
    /// en donde tenemos la manera de saber que es lo que ha ocurrido.
    ///
    static void bw_DoWork(object sender, DoWorkEventArgs e)
    {
        // Cuando hemos llamado a bw.RunWorkerAsync()
        // hemos pasado información al hilo de trabajo.
        // Eso suele ser usado para suministrar al hilo de trabajo
        // información necesaria para que él sea capaz de hacer su tarea.
        string desdeHiloPrincipal = e.Argument.ToString();

        for (int i = 0; i <= 100; i += 20)
        {
            // Cada vez que iteramos,
            // comprobamos si nos han mandado salirnos.
            if (bw.CancellationPending)
            {
                // Si es así, informamos...
                e.Cancel = true;
                // y nos salimos.
                return;
            }

            // Informamos desde aquí acerca del progreso de la operación.
            bw.ReportProgress(i);

            // Aunque estemos en otro hilo diferente,
            // si no ponemos este Sleep aquí,
            // podríamos poner de rodillas al microprocesador,
            // la aplicación podría no responder,
            // obtendríamos justo lo que queremos evitar.
            // Lo que tu pares aquí, es tiempo que concedes al micro
            // para atender al hilo principal de la aplicación.
            Thread.Sleep(1000);
        }
        // Esto se pasa al evento RunWorkerCompleted.
        // Digamos que es el resultado final de la operación.
        // Result es un object, podemos pasar cualquier cosa.
        e.Result = 123;
    }

    ///
    /// bw_ProgressChanged se dispara tras la llamada a bw.ReportProgress(i)
    /// que hemos puesto en bw_DoWork.
    ///
    /// <param name="sender" />
    /// <param name="e" />
    ///
    /// bw_ProgressChanged se dispara en el hilo principal de la aplicación.
    /// Por eso podemos hacer desde él referencia a los controles de nuestra aplicación.
    /// Normalmente suele ser utilizado para actualizar barras de progreso.
    ///
    static void bw_ProgressChanged(object sender,
                                   ProgressChangedEventArgs e)
    {
        Console.WriteLine("Completado " + e.ProgressPercentage + "%");
    }

    ///
    /// Varios pueden ser los motivos por los cuales el proceso terminó.
    /// Desde bw_RunWorkerCompleted tenemos la manera de saber la razón
    /// y actuar en consecuencia.
    /// bw_RunWorkerCompleted también se ejecuta en el hilo principal de
    /// la aplicación, luego desde él también podemos hacer refenrencia
    /// a los controles de nuestra aplicación.
    ///
    /// <param name="sender" />
    /// <param name="e" />
    ///
    /// Puede ser que la operación haya sido cancelada por nosotros mismos.
    /// En ese caso, e.Cancelled = True.
    /// Puede ser que en bw_DoWork haya saltado una excepción.
    /// En ese caso, e.Error != null
    /// O puede ser que la operación haya finalizado sin más.
    ///
    static void bw_RunWorkerCompleted(object sender,
                                      RunWorkerCompletedEventArgs e)
    {
        if (e.Cancelled)
            Console.WriteLine("Tu cancelaste la operación!");
        else if (e.Error != null)
            Console.WriteLine("Excepción en el hilo de trabajo: " +
                              e.Error.ToString());
        else
            // Desde el método DoWork
            Console.WriteLine("Completado - " + e.Result);
    }
}

Subclasing

BackgroundWorker no se trata de una clase sellada, posee un método virtual OnDoWork que sugiere un modo de uso diferente al que acabamos de ver y es que podemos agregar capacidades asíncronas a cualquier clase que hayamos definido haciendo que herede de la clase BackgroundWorker y reemplazando su método OnDoWork. En este caso, la configuración queda encapsulada dentro de nuestra clase y un cliente solo tendría que suscribirse a los eventos ProgressChanged y RunWorkerCompleted. Veamos un ejemplo. Este sería el código fuente de la clase:

using System.ComponentModel;
using System.Threading;

namespace BackgroundWorker
{
    class MiClase : System.ComponentModel.BackgroundWorker
    {
        public MiClase()
        {
            this.WorkerReportsProgress = true;
            this.WorkerSupportsCancellation = true;
        }

        public MiClase(int unDato,
                       string otroDato) : this()
        {

        }

        protected override void OnDoWork(DoWorkEventArgs e)
        {
            // Cuando hemos llamado a bw.RunWorkerAsync()
            // hemos pasado información al hilo de trabajo.
            // Eso suele ser usado para suministrar al hilo de trabajo
            // información necesaria para que él sea capaz de hacer su tarea.
            string desdeHiloPrincipal = e.Argument.ToString();

            for (int i = 0; i <= 100; i += 20)
            {
                // Cada vez que iteramos,
                // comprobamos si nos han mandado salirnos.
                if (this.CancellationPending)
                {
                    // Si es así, informamos...
                    e.Cancel = true;
                    // y nos salimos.
                    return;
                }

                // Informamos desde aquí acerca del progreso de la operación.
                this.ReportProgress(i);

                // Aunque estemos en otro hilo diferente,
                // si no ponemos este Sleep aquí,
                // podríamos poner de rodillas al microprocesador,
                // la aplicación podría no responder,
                // obtendríamos justo lo que queremos evitar.
                // Lo que tu pares aquí, es tiempo que concedes al micro
                // para atender al hilo principal de la aplicación.
                Thread.Sleep(1000);
            }
            // Esto se pasa al evento RunWorkerCompleted.
            // Digamos que es el resultado final de la operación.
            // Result es un object, podemos pasar cualquier cosa.
            e.Result = 123;
        }
    }
}

Y éste sería el código fuente del cliente que hace uso de nuestra nueva clase:

using BackgroundWorker;
using System;
using System.ComponentModel;

class Program
{
    static MiClase miClase;

    static void Main()
    {
        // Configuramos nuestra clase a través del constructor.
        miClase = new MiClase(1, "Cualquier cosa");
        miClase.ProgressChanged += new ProgressChangedEventHandler(miClase_ProgressChanged);
        miClase.RunWorkerCompleted += new RunWorkerCompletedEventHandler(miClase_RunWorkerCompleted);
        miClase.RunWorkerAsync("¡Hola, hilo de trabajo!");

        Console.WriteLine("Presiona ENTER dentro de los próximos 5 segundos para cancelar.");

        Console.ReadLine();

        if (miClase.IsBusy)
            miClase.CancelAsync();

        Console.ReadLine();
    }

    static void miClase_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        Console.WriteLine("Completado " + e.ProgressPercentage + "%");
    }

    static void miClase_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (e.Cancelled)
            Console.WriteLine("Tu cancelaste la operación!");
        else if (e.Error != null)
            Console.WriteLine("Excepción en el hilo de trabajo: " +
                              e.Error.ToString());
        else
            // Desde el método DoWork
            Console.WriteLine("Completado - " + e.Result);
    }
}

Bueno, es otra forma de hacer lo mismo. Este uso que acabamos de ver hace que el patrón asíncrono basado en eventos quede obsoleto.

Anuncios

4 comentarios »

  1. Mi estimadísimo Señor (dado que eres burgués),

    sería usted tan amable de explicarme el porqué de este comentario.

    // Aunque estemos en otro hilo diferente,
    // si no ponemos este Sleep aquí,
    // podríamos poner de rodillas al microprocesador,
    // la aplicación podría no responder,
    // obtendríamos justo lo que queremos evitar.
    // Lo que tu pares aquí, es tiempo que concedes al micro
    // para atender al hilo principal de la aplicación.

    El scheduler del sistema operativo reparte el uso del procesador entre todos los hilos que están listos para ejecutar, respetando las prioridades de cada uno de ellos, y ocasionalmente haciendo boost de los rezagados. Entonces, no veo por qué podría ponerlo de rodillas. Obviamente al hacer un sleep estás renunciando al tiempo que te fue asignado en el procesador, por lo que se produce el efecto de “liberarlo”, y tu hilo principal recibe su turno antes, pero si no lo liberases, el SO le quitará irremediablemente el procesador al hilo una vez que se le acabe el tiempo (quantum) o aparezca un hilo de mayor prioridad (preempting).

    quedo atento a su respuesta.

    Patrick

    Comentario por Patrick — 21 julio 2011 @ 9:53 PM | Responder

    • Muy señor mío:

      Gracias por su comentario. Efectivamente, es como dice.

      Para futuras intervenciones suyas (si las hubiere), puede usted apearme el tratamiento. Como habrá comprobado leyendo la siguiente página: https://elburgues.wordpress.com/burgues (y si no lo ha hecho, yo le invito), el origen del nombre del blog no está en la cuna, sino en la historia de un emigrante en su país.

      Comentario por elburgues — 22 julio 2011 @ 4:08 PM | Responder

  2. GRACIAS Sr. me fue de mucha ayuda el ejemplo y sobretodo la explicación linea a linea …… saludos desde el Tecnológico de Delicias,Chihuahua, México

    Comentario por Gustavo — 13 octubre 2011 @ 9:00 PM | Responder

  3. Hola – Esta muy bien escrito tu articulo – No siempre se encuentra algo tan perfecto. Te felicito. Claro conciso y simple.

    Comentario por Harvey Triana — 8 octubre 2012 @ 3:55 PM | Responder


RSS feed for comments on this post.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

Crea un blog o un sitio web gratuitos con WordPress.com.

A %d blogueros les gusta esto: