VB .NET - Usando o controle BackgroundWorker


Como realizar operações de processamento de dados em segundo plano(de modo assíncrono) e manter a sua interface com o usuário ativa e respondendo a interações dos usuários ?

Boa pergunta , garoto ...

Uma resposta simples e objetiva seria:

Utilize a classe ou controle BackgroundWorker para gerenciar as interações entre o processo principal e thread ativa.

Qual a vantagem de usar este controle se posso eu mesmo fazer o serviço através de threads ? A vantagem é que o componente já oferece eventos que interagem com outras threads, eventos que você teria que criar via código. Como exemplo temos o evento DoWork que é disparado quando é iniciado o trabalho que este terá que fazer e o evento RunWorkerCompleted que é acionado quando o processo assíncrono, que está sendo executado, for terminado.

Veja na tabela abaixo alguns dos eventos deste controle e sua descrição:

Evento Descrição 
DoWork Ocorre quando RunWorkAsync é chamado. Este evento inicia o processamento assíncrono.
ProgressChanged Ocorre quando ReportProgress é chamado.Utilizado fazer uma notificação de progressão do processamento.
RunWorkerCompleted Ocorre quando a operação assíncrona é encerrada, ou quando uma exceção é disparada.


No exemplo deste artigo o código que vamos mostrar inicia uma thread ativa em segundo plano que realiza alguma operação, reportando o seu progresso à thread principal . A thread principal tem a opção de cancelar a thread em segundo plano.

Vamos ao que interessa:

Crie um novo projeto no VB .NET chamado segundoPlanoNet (ou algo mais sugestivo) do tipo Windows Application e inclua os seguintes controles no formulário padrão: form1.vb definindo suas propriedades conforme indicado :

O leiaute do formulário após a inclusão dos controles deve ficar como na figura abaixo:

A seguir as telas mostrando :

fig 1.0 fig 2.0
 
fig 3.0  

A seguir o código para os eventos do componente BackgroundWorker:

Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles atividadeSegundoPlano.DoWork
' ----- O trabalho em segundo plano (background) começa aqui
Dim segundoPlano As BackgroundWorker

' ----- Chama a thread em segundo plano (background)
segundoPlano = CType(sender, BackgroundWorker)
executaTrabalho(segundoPlano)


' ----- Verifica cancelamento
If (segundoPlano.CancellationPending = True) Then e.Cancel = True
End Sub

Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles atividadeSegundoPlano.ProgressChanged
' ----- A tarefa em segundo plano atualiza a barra de progresso
pgbProgressoOperacao.Value = e.ProgressPercentage
End Sub

Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles atividadeSegundoPlano.RunWorkerCompleted
' ----- Termina.
If (e.Cancelled = True) Then
    lblEstadoOperacao.Text = "Operação Cancelada."
Else
     lblEstadoOperacao.Text = "Operação Completa."
End If

pgbProgressoOperacao.Visible = False
pgbProgressoOperacao.Value = 0
btnPararOperacao.Enabled = False
btnIniciaOperacao.Enabled = True

End Sub

O código do relacionado com o evento Click do botão de comando Iniciar Operacão e Parar Operação:

Private Sub btnIniciaOperacao_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnIniciaOperacao.Click
  ' ----- Inicia o processo em segundo plano
  btnIniciaOperacao.Enabled = False
  btnPararOperacao.Enabled = True
  lblEstadoOperacao.Text = "Em Progresso…"
  pgbProgressoOperacao.Value = 0
  pgbProgressoOperacao.Visible = True
  
atividadeSegundoPlano.RunWorkerAsync()
End Sub

Private Sub btnPararOperacao_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnPararOperacao.Click
  ' ----- Informa a thread para parar
  
atividadeSegundoPlano.CancelAsync()

End Sub

O código da rotina executaTrabalho :

Private Sub executaTrabalho(ByVal processoAtivo As BackgroundWorker)
' ----- Realiza algum trabalho
For contador As Integer = 1 To 10
' ----- Verifica se vai sair agora
If (processoAtivo.CancellationPending = True) Then _
   Exit For
  ' ----- Aguarde por 2 segundos.
  Threading.Thread.Sleep(2000)
  ' ----- Informa a thread principal que temos feitos alterações
  processoAtivo.ReportProgress(contador * 10)
Next contador
End Sub

Rode o programa e clique no botão - Iniciar Operação. A barra de progresso será atualizada conforme a operação em segundo plano estiver sendo executada .

Você pode interromper a atividade de segundo plano clicando no botão - Parar Operação.

Os processos executados no Windows possuem a opção de dividir o seu trabalho entre threads de execução separadas dentro destes processos. Por padrão, o processo VB, inclui somente uma thread única de processamento. Porém você pode iniciar uma ou mais threads ativas em segundo plano para realizar alguma atividade à parte do fluxo da aplicação principal.

A .NET Framework inclui o suporte a threads (linhas de execução) através do namespace System.Threading e especificamente através da classe Thread. O uso da classe Thread é simples, mas para poder interagir com outras threads você deverá criar o seu próprio código.

O controle BackgroundWorker é parte do namespace System.ComponentModel, e implementa um conjunto destas interações para você. Para usar este controle você pode simplesmente incluir o controle a partir da barra de ferramentas do VB 2005 ou você também pode usá-lo como uma classe declarando a palavra-chave WithEvents

Private WithEvents BackgroundActivity As System.ComponentModel.BackgroundWorker

Quando você estiver pronto para inciar uma tarefa em segundo plano, chame o método RunWorkerAsync da classe BackgroundWorker. Isto irá disparar o evento DoWork e neste evento chame o método que irá realizar o trabalho em segundo plano. O código acima passa uma instância BackgroundWorker para o método de trabalho. Você não precisa necessariamente passar esta informação mas fazendo isto fica mais fácil a comunicação de retorno para thread primária se ela existir.

Se você quiser que a thread ativa notifique o seu progresso defina a propriedade WorkerReportsProgress como True, então monitore o evento ProgressChanged do controle e chame o método ReportProgress enviando as notificações para a thread principal.

Esta comunicação funciona dos dois lados. Definindo a propriedade WorkerSupportsCancellation do controle como True você permite que a thread primária efetue uma requisição de cancelamento do trabalho via chamada do método CancelAsycn() e isto define a propriedade CancellationPending do controle como vista pela thread de trabalho.

O processamento em segundo plano fica muito simples usando threads , mas as interações entre as threads pode lhe causar muitas dores de cabeça. O problema é que se duas threads desejam atualizar a mesma instância do objeto não há garantia de que elas irão fazer isto em um ordem específica .

Vejamos um exemplo que demonstra esta ocorrência:

Considere uma classe hipotética constituída de 3 membros sendo que a atualização destes membros ocorre sobre múltiplos comandos:

Private instancia As ClasseExemplo

Private Sub atualizaInstancia(ByVal escalar As Integer)
     instancia.Membro1 = 10 * escalar
     instancia.Membro2 = 20 * escalar
     instancia.Membro3 = 30 * escalar
End Sub

Veja o que pode ocorrer quando duas threads diferentes chamam o método atualizaInstancia() ao mesmo tempo (assumindo que eles estão compartilhando a variável instancia. Devido a forma que as threads trabalham é possível que a chamada possa ocorrer de forma intercalada o que implicaria na corrupção dos dados . Suponha que a thread #1 chame atualizaInstancia(2) e a thread #2 chame atualizaInstancia(3). É possível que o comando dentro de atualizaInstancia possa ser chamada na seguinte ordem:

instancia.Membro1 = 10 * 2      ' chamada pela Thread #1
instancia.Membro1 = 10 * 3      ' chamada pela Thread #2

instancia.Membro2 = 20 * 3      ' chamada pela Thread #2
instancia.Membro2 = 20 * 2       ' chamada pela Thread #1

instancia.Membro3 = 30 * 2       ' chamada pela Thread #1
instancia.Membro3 = 30 * 3       ' chamada pela Thread #2

Após a execução acima , Membro1 e Membro3 terão sidos atualizados com com base na chamada da Thread #2 ao passo que Membro2 terá sido atualizada com base na Thread #1

Para prevenir este problema temos a disposição o comando SyncLock que atual como um protetor no bloco de código. Usando SyncLock para o caso acima você precisa criar um objeto comum e usar o mecanismo de bloqueio, vejamos como ficaria o código:

Private instancia As ClasseExemplo
Private bloquearObjeto as New Object
Private Sub atualizaInstancia(ByVal escalar As Integer)
     SyncLock bloquearObjeto 
        instancia.Membro1 = 10 * escalar
        instancia.Membro2 = 20 * escalar
        instancia.Membro3 = 30 * escalar
     End SyncLock
End Sub

Conforme cada thread chame o método atualizaInstancia, o comando SyncLock tenta bloquear exclusivamente a instância bloquearObjeto. Somente quando esta operação ocorre com sucesso o a thread processa o bloco de código.

O exemplo acima não têm utilidade prática alguma a não ser mostrar a forma básica de usar os eventos do controle BackgroundWorker.

Vamos a algo mais interessante...

Nota: Alerta sobre a utilização de threads em aplicações Windows Forms:

Ocorre que os formulários Windows são baseados em um modelo STA - single-threaded apartment; eles podem ser criados em qualquer thread, mas depois que eles foram criados não podem mudar para uma thread diferente, e os métodos dos formulários Windows também não podem ser acessados em outra thread distinta daquela na qual eles foram criados. Isto significa que todos os métodos dos formulários (e os controles criados na mesma thread)  devem ser executados na thread que criou o formulário ou controle do formulário.

Se você tentar chamar um método de um formulário windows em uma thread distinta, uma exceção será disparada, e, dependendo de como você implementou o tratamento de exceção no seu código a aplicação pode encerrar. Se você não estiver efetuando um tratamento de exceção será exibida a seguinte mensagem:

An unhandled exception of type 'System.ArgumentException' occurred in system.windows.forms.dll

Vamos usar o controle BackgroundWorker para contornar este problema.

Crie um novo projeto no VB2005 do tipo Windows Application com o nome de testeAssincrono e a partir da toolbox arraste para o formulário form1.vb o controle BackgroundWorker; a seguir inclua um controle dataGridView e um controle Button conforme o leiaute abaixo:

Declare o seguinte namespace no formulário:

Imports System.Data.sqlclient

a seguir defina a seguinte variável objeto:

Dim dt As DataTable

Agora no evento DoWork do controle BackgroundWorker inclua o seguinte código:

Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork

Try
  
    Using cn As New SqlConnection("Data Source=.\SQLEXPRESS;AttachDbFilename=C:\dados\Cadastro.mdf;Integrated Security=True;Connect Timeout=30;User Instance=True")

       Dim cmd As SqlCommand = New SqlCommand("SELECT * FROM Clientes", cn)
       dt = New DataTable("Clientes")
       Dim da As SqlDataAdapter = New SqlDataAdapter(cmd)
       da.Fill(dt)

  End Using

   SyncLock DataGridView1
         DataGridView1.DataSource = dt
   End SyncLock

Catch ex As Exception
   MsgBox(ex.Message)
End Try
End Sub

e no evento Click do botão de comando inclua o seguinte comando:

Private Sub btnCarregaDados_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnCarregaDados.Click
      BackgroundWorker1.RunWorkerAsync()
End Sub

Tente rodar o projeto e você obterá uma mensagem de erro conforme abaixo:

Não há dúvidas ... Estamos acessando um controle Windows Form a partir de uma thread diferente daquela da qual o controle foi criado.

Para corrigir o problema temos que tratar outro evento : RunWorkerCompleted. Fazendo isto , a chamada de volta já retornou e o acesso ao controle DataGridView ou a qualquer outro controle esta protegida.

Comente ou remova o código do que carregava o datagridView do evento DoWork:

SyncLock DataGridView1
     DataGridView1.DataSource = dt
End SyncLock

Vamos incluir o seguinte código no projeto:

Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
      CarregaDados()
End Sub

Private Sub CarregaDados()
    DataGridView1.DataSource = dt
End Sub

Agora ao rodar o projeto e clicar no botão de comando - Carregar Dados você verá o DataGridView ser carregado sem problema algum:

Pegue o código completo do projeto aqui: backgroundWorker.zip

Até o próximo artigo .NET

Veja os Destaques e novidades do SUPER DVD Visual Basic (sempre atualizado) : clique e confira !

Quer migrar para o VB .NET ?

Quer aprender C# ??

Quer aprender os conceitos da Programação Orientada a objetos ?

Quer aprender o gerar relatórios com o ReportViewer no VS 2013 ?

  Gostou ?   Compartilhe no Facebook   Compartilhe no Twitter

Referências:


José Carlos Macoratti