
Siguiendo con los artículos de programación asíncrona, es turno de hablar de una clase bastante conocida: BackgroundWorker, perteneciente al espacio de nombres System.ComponentModel. Esta clase apareció en la versión 2.0 del .NET Framework. Repasemos entonces la implementación de programación asíncrona con trabajos.
Puedes seguir los otros artículos de la serie con los siguientes enlaces:
Conceptos iniciales
Esta clase es muy práctica, ya que permite ejecutar una tarea en segundo plano, pudiendo a la vez monitorearla y/o cancelarla en cualquier momento. Además, podemos ejecutar un código específico cuando termine el trabajo.
Esta clase encapsula el manejo de los hilos, de esa manera no tenemos que trabajar directamente con las clases Thread, ThreadStart y afines.
Creación y manejo de la clase
Para usar la clase, se agrega el espacio de nombres:
using System.ComponentModel;
La clase se crea de la siguiente manera:
var TrabajoPrueba = new BackgroundWorker();
Las propiedades principales que podemos configurar son las siguientes (todas son tipo booleano):
- CancellationPending: Cuando se solicita la cancelación del trabajo (lo veremos más adelante), hay un tiempo desde la solicitud hasta la ejecución, por lo que esta propiedad indica si hay una cancelación pendiente.
- IsBusy: Indica si el trabajo está en plena ejecución.
- WorkerReportsProgress: Indica si el trabajo puede emitir informes de progreso, es decir, si se generarán o no llamadas al evento ProgressChanged(). Este evento lo veremos más adelante.
- WorkerSupportsCancellation: Indica si el trabajo se puede cancelar de forma asíncrona.
Y entre los eventos principales tenemos:
- DoWork(): Se produce cuando se inicia el trabajo.
- ProgressChanged(): Cada vez que se desea emitir un reporte de progreso del trabajo, se llama a este evento.
- RunWorkerCompleted(): Se produce cuando termina el trabajo, ya sea porque finalizó, se canceló o se produjo una excepción.
Ejemplo simple
Vemos un primer ejemplo simple de consola:
using System;
using System.Linq;
using System.ComponentModel;
namespace ProgramacionAsincrona.Trabajos1
{
class Program
{
public static void Main(string[] args)
{
// Instanciamos el objeto de la clase BackgroundWorker. Además, establecemos que reporte el progreso y que soporte cancelación asíncrona.
var TrabajoPrueba = new BackgroundWorker()
{
WorkerReportsProgress = true,
WorkerSupportsCancellation = true
};
TrabajoPrueba.DoWork += RealizarTrabajo; // Método que hará el trabajo
TrabajoPrueba.ProgressChanged += NotificarProgreso; // Método donde se notificará el progreso
TrabajoPrueba.RunWorkerCompleted += TrabajoCompletado; // Método que se ejecutará al finalizar el trabajo
TrabajoPrueba.RunWorkerAsync(); // Se manda a iniciar el trabajo, de forma asíncrona (segundo plano)
Console.ReadLine();
}
public static void RealizarTrabajo(object sender, DoWorkEventArgs e)
{
// Cuando se inicia el trabajo, hacer lo siguiente:
var TotalTareas = 5; // Establecemos el total de tareas a realizar
var ListaTareas = Enumerable.Range(1, TotalTareas).ToList(); // Simulamos las tareas en una lista
Console.WriteLine("Se inicia el Trabajo de prueba"); // Mensaje de inicio de Trabajo
Console.WriteLine();
foreach (var ProgresoTareas in ListaTareas) // El progreso del trabajo irá del 1 al total de tareas
{
System.Threading.Thread.Sleep(1500); // Una pequeña pausa que simula la tarea. además de poder ver el efecto de progreso en la consola
((BackgroundWorker)sender).ReportProgress((int)(ProgresoTareas * 100d / TotalTareas)); // Al finalizar cada tarea, reportamos el progreso (Porcentaje, valor entero del 1 al 100)
}
}
public static void NotificarProgreso(object sender, ProgressChangedEventArgs e)
{
// Cada vez que se notifica un progreso, hacer lo siguiente:
Console.WriteLine(string.Format("Se completó el Trabajo de prueba al {0}%", e.ProgressPercentage.ToString()));
}
public static void TrabajoCompletado(object sender, RunWorkerCompletedEventArgs e)
{
// Si terminó el trabajo (finalización, cancelación, error), hacer lo siguiente:
Console.WriteLine();
Console.WriteLine("Finaliza el Trabajo de prueba");
}
}
}
Observación: los métodos son estáticos porque estamos probando desde el procedimiento principal del aplicativo de consola.
Al ejecutar el programa se verá lo siguiente:

Cada tarea imprime su avance en pantalla. Al finalizar el total de tareas (100%), se imprime el mensaje de fin de trabajo. Obviamente este ejemplo no tiene mucho sentido, pero en un caso real la idea es que mientras se ejecuta el trabajo en segundo plano, el programa principal haga otras tareas.
Ejemplo simple con cancelación
Ahora modificaremos un poco el código para que en la segunda iteración se cancele el trabajo. Ojo que por tema de practicidad, la cancelación se está dando desde el mismo método RealizarTrabajo, en la vida real es muy probable que la cancelación se dé desde otro método o a través de un click de un botón, por ejemplo.
Además, es importante hacer notar que se está poniendo una validación en el método RealizarTrabajo, que consulta si existe una cancelación pendiente, para abandonar la ejecución si es el caso (línea 38).
using System;
using System.Linq;
using System.ComponentModel;
namespace ProgramacionAsincrona.Trabajos1
{
class Program
{
public static void Main(string[] args)
{
// Instanciamos el objeto de la clase BackgroundWorker. Además, establecemos que reporte el progreso y que soporte cancelación asíncrona.
var TrabajoPrueba = new BackgroundWorker()
{
WorkerReportsProgress = true,
WorkerSupportsCancellation = true
};
TrabajoPrueba.DoWork += RealizarTrabajo; // Método que hará el trabajo
TrabajoPrueba.ProgressChanged += NotificarProgreso; // Método donde se notificará el progreso
TrabajoPrueba.RunWorkerCompleted += TrabajoCompletado; // Método que se ejecutará al finalizar el trabajo
TrabajoPrueba.RunWorkerAsync(); // Se manda a iniciar el trabajo, de forma asíncrona (segundo plano)
Console.ReadLine();
}
public static void RealizarTrabajo(object sender, DoWorkEventArgs e)
{
// Cuando se inicia el trabajo, hacer lo siguiente:
var TotalTareas = 5; // Establecemos el total de tareas a realizar
var ListaTareas = Enumerable.Range(1, TotalTareas).ToList(); // Simulamos las tareas en una lista
Console.WriteLine("Se inicia el Trabajo de prueba"); // Mensaje de inicio de Trabajo
Console.WriteLine();
foreach (var ProgresoTareas in ListaTareas) // El progreso del trabajo irá del 1 al total de tareas
{
if (((BackgroundWorker)sender).CancellationPending) return; // Se valida si se ha cancelado el trabajo. En caso afirmativo, abandonar la ejecución
System.Threading.Thread.Sleep(1500); // Una pequeña pausa que simula la tarea. además de poder ver el efecto de progreso en la consola
((BackgroundWorker)sender).ReportProgress((int)(ProgresoTareas * 100d / TotalTareas)); // Al finalizar cada tarea, reportamos el progreso (Porcentaje, valor entero del 1 al 100)
if (ProgresoTareas == 2)
{
((BackgroundWorker)sender).CancelAsync(); // Al llegar a la segunda iteración se cancela el trabajo
}
}
}
public static void NotificarProgreso(object sender, ProgressChangedEventArgs e)
{
// Cada vez que se notifica un progreso, hacer lo siguiente:
Console.WriteLine(string.Format("Se completó el Trabajo de prueba al {0}%", e.ProgressPercentage.ToString()));
}
public static void TrabajoCompletado(object sender, RunWorkerCompletedEventArgs e)
{
// Si terminó el trabajo (finalización, cancelación, error), hacer lo siguiente:
Console.WriteLine();
Console.WriteLine("Finaliza el Trabajo de prueba");
}
}
}
Al ejecutar el programa se verá lo siguiente:

Se observa que luego de la segunda ejecución el programa termina sin realizar las demás tareas.
Acceso a recursos en diferentes hilos de ejecución
Hay una consideración importante cuando trabajemos con un caso real. Vamos con un ejemplo para entenderlo mejor: tenemos un formulario simple que hará lo mismo que el primer ejemplo, con un botón (Button: btnIniciar) para iniciar el trabajo; y una lista (Listview: lstEventos) donde se imprimirán los mensajes.
El código es el siguiente:
using System;
using System.Linq;
using System.Windows.Forms;
using System.ComponentModel;
namespace ProgramacionAsincrona.Trabajos2
{
public partial class FormPrueba : Form
{
public FormPrueba()
{
InitializeComponent();
}
private void btnIniciarTrabajo_Click(object sender, EventArgs e)
{
var TrabajoPrueba = new BackgroundWorker()
{
WorkerReportsProgress = true,
WorkerSupportsCancellation = true
};
TrabajoPrueba.DoWork += RealizarTrabajo; // Método que hará el trabajo
TrabajoPrueba.ProgressChanged += NotificarProgreso; // Método donde se notificará el progreso
TrabajoPrueba.RunWorkerCompleted += TrabajoCompletado; // Método que se ejecutará al finalizar el trabajo
TrabajoPrueba.RunWorkerAsync(); // Se manda a iniciar el trabajo, de forma asíncrona (segundo plano)
}
public void RealizarTrabajo(object sender, DoWorkEventArgs e)
{
// Cuando se inicia el trabajo, hacer lo siguiente:
var TotalTareas = 5; // Establecemos el total de tareas a realizar
var ListaTareas = Enumerable.Range(1, TotalTareas).ToList(); // Simulamos las tareas en una lista
this.lstEventos.Items.Add("Se inicia el Trabajo de prueba"); // Mensaje de inicio de Trabajo
foreach (var ProgresoTareas in ListaTareas) // El progreso del trabajo irá del 1 al total de tareas
{
System.Threading.Thread.Sleep(1500); // Una pequeña pausa que simula la tarea. además de poder ver el efecto de progreso en la ventana
((BackgroundWorker)sender).ReportProgress((int)(ProgresoTareas * 100d / TotalTareas)); // Al finalizar cada tarea, reportamos el progreso (Porcentaje, valor entero del 1 al 100)
}
}
public void NotificarProgreso(object sender, ProgressChangedEventArgs e)
{
// Cada vez que se notifica un progreso, hacer lo siguiente:
this.lstEventos.Items.Add(string.Format("Se completó el Trabajo de prueba al {0}%", e.ProgressPercentage.ToString()));
}
public void TrabajoCompletado(object sender, RunWorkerCompletedEventArgs e)
{
// Si terminó el trabajo (finalización, cancelación, error), hacer lo siguiente:
this.lstEventos.Items.Add("Finaliza el Trabajo de prueba");
}
}
}
Debido a que el método que se enlaza al evento DoWork se ejecuta en un hilo diferente (segundo plano) al principal, no podemos acceder a los controles (Button, TextBox, Grid, ListView, etc.) desde dicho método. Cuando ejecutemos el programa y hagamos click en el botón para iniciar el trabajo, nos aparecerá el siguiente error:

Para solucionar esto, cuando queremos acceder a los controles tenemos que hacerlo a través de un delegado. Podemos ver el primer artículo de esta serie aquí para mayor información, o podemos optar por una solución violenta, reemplazando la línea 36 de nuestro código de esta manera:
this.Invoke((MethodInvoker)delegate()
{
this.lstEventos.Items.Add("Se inicia el Trabajo de prueba"); // Mensaje de inicio de Trabajo
});
Lo que estamos haciendo es invocar a un método anónimo desde el programa principal. El método es todo lo que está dentro de delegate() { } y this es el formulario, por lo que en otros escenarios donde el código a ejecutar esté en otra clase, no olvidar siempre tener una referencia a nuestro formulario.
Entonces problema solucionado, el programa ejecuta correctamente:

La llamada al método anónimo funciona desde la versión 2.0 del .NET Framework. Además, los eventos ProgressChanged y RunWorkerCompleted sí se ejecutan en el hilo principal, por lo que no es necesario esta consideración en esos casos.
Código de ejemplo en CodePlex: Programación asíncrona 3 – Trabajos (BackgroundWorker)
Conclusiones
Como podemos ver, es una clase muy útil. Creando una buena estructura, tendremos una manera legible y elegante de manejar procesos en segundo plano sin acceder directamente a los hilos. Para el siguiente artículo de esta serie nos toca hablar de un tema muy interesante, una implementación de asincronía que viene en las versiones más recientes del .NET Framework.