This article is currently in the process of being translated into Spanish (~99% done).
Multi-threading with the BackgroundWorker
Por defecto, cada vez que su aplicación ejecuta un fragmento de código, este código se ejecuta en el mismo hilo que la aplicación misma. Esto significa que mientras esto el código se está ejecutando, no sucede nada más dentro de su aplicación, incluida la actualización de su interfaz de usuario.
Esto es una sorpresa para las personas que son nuevas en la programación de Windows, cuando hacen algo que lleva más de un segundo y se dan cuenta de que la aplicación realmente se cuelga mientras lo hace. El resultado es una gran cantidad de publicaciones frustradas en el foro de personas que intentan ejecutar un proceso prolongado mientras se actualizan una barra de progreso, solo para darse cuenta de que la barra de progreso no se actualiza hasta que el proceso termina de ejecutarse.
La solución a todo esto es el uso de múltiples subprocesos, y aunque C# hace que esto sea bastante fácil de hacer, los subprocesos múltiples vienen con MUCHAS trampas, y para mucha gente, simplemente no son tan comprensibles. Aquí es donde entra en juego el BackgroundWorker : lo hace simple, fácil y rápido para trabajar con un hilo adicional en su aplicación.
Cómo funciona el BackgroundWorker
El concepto más difícil sobre el subprocesamiento múltiple en una aplicación de Windows es el hecho de que no puede realizar cambios en la interfaz de usuario desde otro hilo: si usted lo hace, la aplicación se bloqueará inmediatamente. En su lugar, debe invocar un método en el subproceso UI (principal) que luego hace los cambios deseado. Todo esto es un poco engorroso, pero no cuando usas el BackgroundWorker.
Al realizar una tarea en un hilo diferente, generalmente tiene que comunicarse con el resto de la aplicación en dos situaciones: cuando desea actualizar para mostrar lo que te queda de proceso, y luego, por supuesto, cuando se realiza la tarea y desea mostrar el resultado. El BackgroundWorker está construido alrededor de esta idea, y por lo tanto viene con los dos eventos ProgressChanged y RunWorkerCompleted.
El tercer evento se llama DoWork y la regla general es que no puede tocar nada en la interfaz de usuario de este evento. En su lugar, llama al método ReportProgress () , que a su vez genera el evento ProgressChanged , desde donde puede actualizar la IU. Una vez finalizado, asignas un resultado del trabajo y se genera el evento RunWorkerCompleted .
En resumen, el evento DoWork se encarga de todo el trabajo duro. Todo ese código se ejecuta en un hilo diferente y es por esta razón por la que no puede tocar la interfaz de usuario desde allí. En su lugar, recuperas datos (de la interfaz de usuario o de otro lugar) dentro del evento utilizando el argumento en RunWorkerAsync (), y los datos resultantes asignándolos a la propiedad e.Result.
Los eventos ProgressChanged y RunWorkerCompleted , por otro lado, otro lado, se ejecutan en el mismo subproceso en el que se crea el BackgroundWorker, que generalmente será el subproceso principal / UI y, por lo tanto, puede actualizar la UI desde allí. Por lo tanto, la única comunicación que se puede realizar entre la tarea en segundo plano y la IU es a través del metodo ReportProgress()
Esa era una gran teoría, pero aunque BackgroundWorker es fácil de usar, es importante entender cómo y qué hace, para no incurrir accidentalmente en hacer algo mal: como ya se dijo, los errores en subprocesos múltiples pueden provocar algunos problemas desagradables.
Un ejemplo BackgroundWorker
Basta con la teoría, veamos ahora de qué se trata. En este primer ejemplo, queremos realizar un trabajo bastante simple pero que requiera mucho tiempo. Cada número entre 0 y 10.000 se prueba para ver si es divisible con el número 7. Esto es realmente pan comido para las computadoras rápidas de hoy en día, por lo que para que sea más lento y, por lo tanto, más fácil probar nuestro punto, he agregado un un retraso de milisegundos en cada una de las iteraciones.
Nuestra aplicación de muestra tiene dos botones: uno que realizará la tarea sincrónicamente (en el mismo hilo) y otro que realizará la tarea con un BackgroundWorker y, por lo tanto, en un hilo diferente. Esto debería hacer que sea muy fácil ver la necesidad de un hilo adicional al realizar tareas que requieren mucho tiempo. El código se vería así:
<Window x:Class="WpfTutorialSamples.Misc.BackgroundWorkerSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="BackgroundWorkerSample" Height="300" Width="375">
<DockPanel Margin="10">
<DockPanel DockPanel.Dock="Top">
<Button Name="btnDoSynchronousCalculation" Click="btnDoSynchronousCalculation_Click" DockPanel.Dock="Left" HorizontalAlignment="Left">Synchronous (same thread)</Button>
<Button Name="btnDoAsynchronousCalculation" Click="btnDoAsynchronousCalculation_Click" DockPanel.Dock="Right" HorizontalAlignment="Right">Asynchronous (worker thread)</Button>
</DockPanel>
<ProgressBar DockPanel.Dock="Bottom" Height="18" Name="pbCalculationProgress" />
<ListBox Name="lbResults" Margin="0,10" />
</DockPanel>
</Window>
using System;
using System.ComponentModel;
using System.Windows;
namespace WpfTutorialSamples.Misc
{
public partial class BackgroundWorkerSample : Window
{
public BackgroundWorkerSample()
{
InitializeComponent();
}
private void btnDoSynchronousCalculation_Click(object sender, RoutedEventArgs e)
{
int max = 10000;
pbCalculationProgress.Value = 0;
lbResults.Items.Clear();
int result = 0;
for(int i = 0; i < max; i++)
{
if(i % 42 == 0)
{
lbResults.Items.Add(i);
result++;
}
System.Threading.Thread.Sleep(1);
pbCalculationProgress.Value = Convert.ToInt32(((double)i / max) * 100);
}
MessageBox.Show("Numbers between 0 and 10000 divisible by 7: " + result);
}
private void btnDoAsynchronousCalculation_Click(object sender, RoutedEventArgs e)
{
pbCalculationProgress.Value = 0;
lbResults.Items.Clear();
BackgroundWorker worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.DoWork += worker_DoWork;
worker.ProgressChanged += worker_ProgressChanged;
worker.RunWorkerCompleted += worker_RunWorkerCompleted;
worker.RunWorkerAsync(10000);
}
void worker_DoWork(object sender, DoWorkEventArgs e)
{
int max = (int)e.Argument;
int result = 0;
for(int i = 0; i < max; i++)
{
int progressPercentage = Convert.ToInt32(((double)i / max) * 100);
if(i % 42 == 0)
{
result++;
(sender as BackgroundWorker).ReportProgress(progressPercentage, i);
}
else
(sender as BackgroundWorker).ReportProgress(progressPercentage);
System.Threading.Thread.Sleep(1);
}
e.Result = result;
}
void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
pbCalculationProgress.Value = e.ProgressPercentage;
if(e.UserState != null)
lbResults.Items.Add(e.UserState);
}
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
MessageBox.Show("Numbers between 0 and 10000 divisible by 7: " + e.Result);
}
}
}
La parte XAML consta de un par de botones, uno para ejecutar el proceso de forma sincrónica (en el subproceso de la interfaz de usuario) y otro para ejecutarlo de forma asincrónica (en un subproceso de fondo), un control ListBox para mostrar todos los números calculados y luego un control ProgressBar en la parte inferior de la ventana para mostrar ... bien, ¡el progreso!
En el código subyacente, comenzamos con el controlador de eventos síncronos. Como se mencionó, se repite de 0 a 10.000 con un pequeño retraso en cada iteración, y si el número es divisible con el número 7, luego lo agregamos a la lista. En cada iteración también actualizamos la barra de progreso, y una vez que hayamos terminado, mostramos un mensaje al usuario sobre cuántos números se encontraron.
Si ejecuta la aplicación y presiona el primer botón, se verá así, no importa cuán lejos esté en el proceso:
No hay elementos en la lista y, no hay progreso en la barra de progreso, y el botón ni siquiera se ha lanzado, lo que demuestra que no ha habido una sola actualización para la interfaz de usuario desde que se presionó el ratón sobre el botón.
Al presionar el segundo botón, se usará el enfoque BackgroundWorker. Como puede ver en el código, hacemos más o menos lo mismo, pero en un poco diferente modo. Todo el trabajo duro ahora se coloca en el evento DoWork , que el trabajador llama después de ejecutar el método RunWorkerAsync () . Este método toma información de su aplicación que puede ser utilizada por el trabajador, como veremos más adelante.
Como ya se mencionó, no podemos actualizar la interfaz de usuario del evento DoWork. En su lugar, llamamos al método ReportProgress en el trabajador. Si el número actual es divisible por 7, lo incluimos para agregarlo a la lista; de lo contrario, solo informaremos del porcentaje de progreso actual, de modo que el ProgressBar puede ser actualizado.
Una vez que todos los números han sido probados, asignamos el resultado a la propiedad e.Result. Esto se llevará al evento RunWorkerCompleted , donde se lo mostramos al usuario. Esto puede parecer un poco engorroso, en lugar de mostrárselo al usuario tan pronto como termine el trabajo, pero una vez más, asegura que no nos comuniquemos con la interfaz de usuario desde el evento DoWork , que no está permitido.
El resultado es, como puede ver, mucho más fácil de usar:
La ventana ya no se cuelga, se hace clic en el botón pero no se suprime, la lista de números posibles se actualiza sobre la marcha y la barra de progreso avanza constantemente: la interfaz se ha vuelto mucho más receptiva.
Entrada y salida
Observe que tanto la entrada, en forma del argumento pasado al método RunWorkerAsync () , como la salida, en forma del valor asignados a la propiedad e.Result del evento DoWork, son del tipo de objeto. Esto significa que puede asignarles cualquier tipo de valor. Nuestro ejemplo fue básico, ya que tanto la entrada como la salida podrían estar contenidas en un solo valor entero, pero es normal tener una entrada y salida más compleja.
Esto se logra mediante el uso de un tipo más complejo, en muchos casos una estructura o incluso una clase que usted mismo crea y transmite. Al hacer esto, las posibilidades son infinitas y puede transportar muchos y datos tan complejos como desee entre su BackgroundWorker y su aplicación / IU.
Lo mismo es realmente cierto para el método ReportProgress. Su argumento secundario se llama userState y es un tipo de objeto, lo que significa que puede pasar lo que quieras con el método ProgressChanged.
Resumen
BackgroundWorker es una herramienta excelente cuando desea múltiples subprocesos en su aplicación, principalmente porque es muy fácil de usar. En este capítulo hemos visto una de las cosas facilitadas por BackgroundWorker, que es el informe de progreso, pero el soporte para cancelar la tarea en ejecución es muy útil también. Lo veremos en el próximo capítulo.