This article is currently in the process of being translated into Portuguese (~99% done).
Multi-threading with the BackgroundWorker
Por padrão, toda vez que seu aplicativo executa um trecho de código, esse código é executado no mesmo thread que o próprio aplicativo. Isso significa que, enquanto esse código está em execução, nada mais acontece dentro do seu aplicativo, incluindo a atualização da sua interface do usuário.
Isso é uma grande surpresa para as pessoas que são novas na programação do Windows, quando fazem algo que leva mais de um segundo e percebem que o aplicativo realmente falha ao fazer isso. O resultado são muitas postagens frustradas em fóruns de pessoas que estão tentando executar um processo demorado enquanto atualizam uma barra de progresso, apenas para perceber que a barra de progresso não é atualizada até que o processo seja executado.
A solução para tudo isso é o uso de vários encadeamentos e, embora o C # torne isso muito fácil de ser feito, o multiencadeamento vem com MUITAS armadilhas e, para muitas pessoas, elas não são tão compreensíveis. É aí que o BackgroundWorker entra em ação - torna simples, fácil e rápido trabalhar com um thread extra em seu aplicativo.
Como o BackgroundWorker funciona
O conceito mais difícil sobre multi-threading em um aplicativo do Windows é o fato de que você não tem permissão para fazer alterações na interface do usuário de outro thread - se você fizer isso, o aplicativo irá falhar imediatamente. Em vez disso, você precisa invocar um método no thread da interface do usuário (principal) que, em seguida, faz as alterações desejadas. Isso é um pouco incômodo, mas não quando você usa o BackgroundWorker.
Ao executar uma tarefa em um thread diferente, você geralmente precisa se comunicar com o restante do aplicativo em duas situações: Quando quiser atualizá-lo para mostrar o quão longe você está no processo e, claro, quando a tarefa estiver concluída e você quer mostrar o resultado. O BackgroundWorker é construído em torno dessa ideia e, portanto, vem com os dois eventos ProgressChanged e RunWorkerCompleted.
O terceiro evento é chamado DoWork e a regra geral é que você não pode tocar em nada na interface do usuário deste evento. Em vez disso, você chama o método ReportProgress () , que, por sua vez, gera o evento ProgressChanged , de onde você pode atualizar a interface do usuário. Quando terminar, você atribui um resultado ao funcionário e, em seguida, o evento RunWorkerCompleted é gerado.
Então, para resumir, o evento DoWork cuida de todo o trabalho duro. Todo o código ali é executado em um thread diferente e, por essa razão, você não tem permissão para tocar na interface do usuário a partir dele. Em vez disso, você traz dados (da interface do usuário ou de outro lugar) para o evento usando o argumento no método RunWorkerAsync () e os dados resultantes, atribuindo-os à propriedade e.Result.
Os eventos ProgressChanged e RunWorkerCompleted , por outro lado, são executados no mesmo thread que o BackgroundWorker é criado on, que geralmente será o thread / UI principal e, portanto, você tem permissão para atualizar a UI deles. Portanto, a única comunicação que pode ser executada entre a tarefa em segundo plano em execução e a interface do usuário é por meio do método ReportProgress ().
Isso foi muita teoria, mas mesmo que o BackgroundWorker seja fácil de usar, é importante entender como e o que ele faz, para que você não faça algo errado acidentalmente - como já foi dito, erros em multi-threading podem levar a algum problemas desagradáveis.
Um exemplo do BackgroundWorker
Chega com a teoria - vamos ver o que é tudo isso. Neste primeiro exemplo, queremos um trabalho simples, mas demorado, realizado. Cada número entre 0 e 10.000 é testado para ver se é divisível com o número 7. Na verdade, é um pedaço de bolo para os computadores rápidos de hoje, por isso, para consumir mais tempo e, portanto, mais fácil de provar nosso ponto, adicionei um um milissegundo de atraso em cada uma das iterações.
Nosso aplicativo de amostra tem dois botões: um que executará a tarefa de forma síncrona (no mesmo thread) e um que executará a tarefa com um BackgroundWorker e, portanto, em um thread diferente. Isso deve facilitar a visualização da necessidade de um thread extra ao executar tarefas demoradas. O código é assim:
<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);
}
}
}
A parte XAML consiste em alguns botões, um para executar o processo de forma síncrona (no thread da interface do usuário) e outro para executá-lo de forma assíncrona (em um thread de segundo plano), um controle ListBox para mostrar todos os números calculados e um controle ProgressBar a parte inferior da janela para mostrar ... bem, o progresso!
Em Code-behind, começamos com o manipulador de eventos síncrono. Como mencionado, ele dá um loop de 0 a 10.000 com um pequeno atraso em cada iteração, e se o número for divisível com o número 7, então o adicionamos à lista. Em cada iteração, também atualizamos a ProgressBar e, assim que terminarmos, mostramos ao usuário uma mensagem sobre quantos números foram encontrados.
Se você executar o aplicativo e pressionar o primeiro botão, ele ficará assim, não importa o quão longe você esteja no processo:
Nenhum item na lista e nenhum progresso na ProgressBar, e o botão nem foi lançado, o que prova que não houve uma única atualização na interface do usuário desde que o mouse foi pressionado sobre o botão.
Pressionar o segundo botão, em vez disso, usará a abordagem BackgroundWorker. Como você pode ver no código, fazemos praticamente o mesmo, mas de uma maneira um pouco diferente. Todo o trabalho pesado agora é colocado no evento DoWork , que o worker chama depois de executar o método RunWorkerAsync () . Esse método usa a entrada do seu aplicativo, que pode ser usada pelo funcionário, como falaremos mais adiante.
Como já mencionado, não podemos atualizar a interface do usuário do evento DoWork. Em vez disso, chamamos o método ReportProgress no worker. Se o número atual é divisível com 7, nós o incluímos para ser adicionado na lista - caso contrário, nós só informamos a porcentagem atual de progresso, para que a ProgressBar possa ser atualizada.
Depois que todos os números tiverem sido testados, atribuímos o resultado à propriedade e.Result. Isso será levado ao evento RunWorkerCompleted , onde será exibido para o usuário. Isso pode parecer um pouco complicado, em vez de apenas mostrá-lo ao usuário assim que o trabalho é concluído, mas, mais uma vez, garante que não nos comunicaremos com a interface do usuário a partir do evento DoWork . o que não é permitido.
O resultado é, como você pode ver, muito mais amigável:
A janela não trava mais, o botão é clicado, mas não suprimido, a lista de possíveis números é atualizada na hora e a ProgressBar está subindo continuamente - a interface ficou muito mais responsiva.
Entrada e saída
Observe que a entrada, na forma do argumento transmitida ao método RunWorkerAsync () , bem como a saída, na forma do valor atribuído ao e.Result propriedade do evento DoWork, são do tipo de objeto. Isso significa que você pode atribuir qualquer tipo de valor a eles. Nosso exemplo foi básico, já que tanto a entrada quanto a saída podem estar contidas em um único valor inteiro, mas é normal ter entradas e saídas mais complexas.
Isso é feito usando um tipo mais complexo, em muitos casos, uma estrutura ou até mesmo uma classe que você cria e repassa. Ao fazer isso, as possibilidades são praticamente infinitas e você pode transportar o máximo de dados complexos que quiser entre seu BackgroundWorker e seu aplicativo / UI.
O mesmo é verdade para o método ReportProgress. Seu argumento secundário é chamado userState e é um tipo de objeto, o que significa que você pode passar o que quiser para o método ProgressChanged.
Resumo
O BackgroundWorker é uma excelente ferramenta quando você quer multi-threading em sua aplicação, principalmente porque é muito fácil de usar. Neste capítulo, observamos uma das coisas facilitadas pelo BackgroundWorker, que é o relatório de progresso, mas o suporte para cancelar a tarefa em execução também é muito útil. Analisaremos isso no próximo capítulo.