BackGroundWorkers: Multi-Threading ao alcance de todos
Introdução
Imagine-se por uns instantes proprietário de uma fábrica de teclados. Simplificando os processos, digamos que bastam 5 funcionários para construír um teclado. Um funcionário molda a base, o segundo molda as teclas, o terceiro coloca o decalque gráfico nas teclas, um quarto assembla os componentes electrónicos e um quinto monta todas as partes. Se cada processo demorar uma hora, conseguimos um teclado em duas horas. Moldar, colar e assemblar electrónica são todas executadas simultaneamente e todas duram uma hora. Acrescentamos no final a hora que o quinto funcionário demora a montar todas as peças.
Quem molda não pode assemblar electrónica, e vice-versa, ao mesmo tempo, pois prejudicariam os seus desempenhos específicos. Mas porque temos 5 funcionários, reduzimos os processos de 5 horas combinadas para 2.
Se ao invés de 5 funcionários, contratarmos apenas um, cada tarefa terá que ser executada independentemente uma da outra, passando a demorar 5 horas a completar um teclado.
Compreender a analogia é essencial para entender o que são threads.
O que são threads afinal?
Voltando a pegar na analogia da introdução, se substituírmos a palavra funcionário por thread, continuamos a obter um sentido das frases. Um thread nada mais é do que "outro par de mãos" para executar outros trabalhos.
Passando para a nossa realidade, o user interface que criamos na IDE do VB e todo o código que escrevemos nas suas Subs, estão a ser executadas apenas por um funcionário, uma thread, vulgarmente chamada de UI Thread. (UIT daqui em diante).
A UIT é responsável por a execução do código que escrevemos nos nossos forms. E porque a colocação gráfica dos nossos componentes assenta primeiramente no nosso form (antes visível, agora os designers), é natural que sejam também estes tratados pelo UIT.
O problema
O grande problema com que nos deparamos é que ao executar tarefas que requerem processamento complexo, a UIT bloqueia simplesmente. Fica tão concentrada naquele processamento específico que deixa a apresentação gráfica e tudo o que dela deriva para segundo plano. Perder o controlo e a noção do que está a acontecer é um factor negativo para o utilizador, que terá a tendência de tentar descobrir o que se está a passar podendo isto traduzir-se em mais carga para o UIT.
Assíncrono? Síncrono?
São estes, termos que se usam com bastante frequência para nos referirmos a chamadas. As chamadas síncronas são feitas para o mesmo thread que as chama. Chamam-se síncronas porque a sua execução depende da rapidez com que o actual thread executa a sua actual tarefa. As chamadas asíncronas são feitas para outro thread, libertando o thread que a chamou da carga que esta chamada iria provocar, para o novo thread. Chamam-se assíncronas porque não dependem de uma sequência e são executadas imediatamente.
Eis um exemplo de um processo síncrono, executado a partir do UIT:
For i As Long = 0 To 9999
Debug.Print("Este é o print #" & i)
Next
Correr este bloco de código a partir do UIT poderá demorar minutos, e durante esse tempo, o form vai parecer estar "bloqueado".
Facilitando a gestão
Trabalhar e gerir multiplos threads (multi-threading) pode ser uma tarefa complicada e até ineficaz se mal concebida. Para iniciar o conceito, tornando-o acessível até a quem não está inteiramente familiarizado com multi-threading, existem os BackGroundWorkers, literalmente Trabalhadores de segundo plano.
Da teoria à prática
Comecemos por criar uma nova instância do BackGroundWorker
:
Private WithEvents BGW As New BackgroundWorker
Nota: BackGroundWorker
encontra-se sobre o namespace System.ComponentModel
.
De seguida vamos escrever o código que queremos que a nova thread execute, dentro do sub levantado por o evento DoWork
:
Private Sub BGW_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BGW.DoWork
Dim Max As Long = e.Argument
For i As Long = 0 To Max
If BGW.CancellationPending = True Then
e.Cancel = True
Else
Debug.Print("Este é o print #" & i)
BGW.ReportProgress(CInt((i * 100) / Max))
End If
Next
If BGW.CancellationPending = False Then e.Result = Max & " números completos!"
End Sub
O que queremos que altere quando é reportado progresso:
Private Sub BGW_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BGW.ProgressChanged
ProgressBar1.Value = e.ProgressPercentage
End Sub
E o que acontece depois da tarefa terminar:
Private Sub BGW_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BGW.RunWorkerCompleted
If e.Cancelled = True Then
MsgBox("Cancelado por o utilizador...")
Else
MsgBox("Completei o meu trabalho!" & vbCrLf & e.Result)
End If
End Sub
Introduziram-se subs para os 3 principais eventos de um BackGroundWorker
.
Subs | Descrição |
---|---|
DoWork |
é um evento disparado quando se ordena o BackGroundWorker executar o seu trabalho. |
RunWorkerCompleted |
é o evento disparado quando o sub que lida com o evento DoWork chega ao final. |
ProgressChanged |
é o evento disparado sempre que se ordena um relatório de progresso. É possível afectar o UIT neste sub. |
Reportar, cancelar
Durante a execução do trabalho é possível colocar estratégicamente ordens para reportar o progresso do trabalho, em forma de percentagem.
BGW.ReportProgress(CInt((i * 100) / Max))
A linha acima, colocada dentro do ciclo, fará com que o evento ProgressChanged
seja disparado a cada novo número, calculando a percentagem através de cálculos aritméticos e passando este valor para um elemento visual, a tratar pela UIT. Neste caso a nossa progress bar. E porque por vezes é necessário cancelar um trabalho, os BackGroundWorker
s também podem ser preparados para suportar cancelamento de tarefas.
BGW.CancelAsync()
O método acima fará com que uma ordem de cancelamento seja passada ao thread que está encarregue do código em execução no sub que lida com o DoWork
. Depois da ordem de cancelamento ser entregue, é responsabilidade do thread que está a trabalhar, verificar "periodica e estratégicamente" se existe alguma ordem de cancelamento.
If BGW.CancellationPending = True Then e.Cancel = True
A linha acima verifica se existe ordem para cancelamento e se existir, forçamos o fluxo até ao final do sub que contém o trabalho, ignorando o processamento complexo que está a tratar, causando o disparo do evento RunWorkerCompleted
, cujo sub que com ele lida verifica se houve uma ordem de cancelamento ou se o trabalho está pura e simplesmente terminado.
If e.Cancelled = True Then
MsgBox("Cancelado por o utilizador...")
Else
MsgBox("Completei o meu trabalho!" & vbCrLf & e.Result)
End If
Como o BackGroundWorker
e a tarefa que queremos executar estão perfeitamente funcionais e a reportar progressos, está na altura de configurarmos a instância para dar início ao seu trabalho.
ProgressBar1.Maximum = 100
BGW.WorkerReportsProgress = True
BGW.WorkerSupportsCancellation = True
BGW.RunWorkerAsync(9999)
Propriedade | Descrição |
---|---|
WorkerReportsProgress |
determina se esta instância será capaz de reportar os seus progressos. |
WorkerSupportsCancellation |
determina se esta instância tem a possibilidade de cancelar o seu trabalho. |
O método RunWorkerAsync()
dá início à execução do trabalho.
Importante: O argumento, 9999 neste caso, é passado para a thread do trabalho e pode ser resgatada através da propriedade Argument
dos argumentos (e.Argument
). Trata-se de uma variável do tipo Object
para permitir a passagem de qualquer objecto
Reproduzir o exemplo deste artigo
No form de "startup" introduzir:
2x Button 1x ProgressBar
E substituír todo o código da form por o código abaixo.
Imports System.ComponentModel
Public Class Form1
Private WithEvents BGW As New BackgroundWorker
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
ProgressBar1.Maximum = 100
BGW.WorkerReportsProgress = True
BGW.WorkerSupportsCancellation = True
BGW.RunWorkerAsync(9999)
End Sub
Private Sub BGW_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BGW.DoWork
Dim Max As Long = e.Argument
For i As Long = 0 To Max
If BGW.CancellationPending = True Then
e.Cancel = True
Else
Debug.Print("Este é o print #" & i)
BGW.ReportProgress(CInt((i * 100) / Max))
End If
Next
If BGW.CancellationPending = False Then e.Result = Max & " números completos!"
End Sub
Private Sub BGW_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BGW.ProgressChanged
ProgressBar1.Value = e.ProgressPercentage
End Sub
Private Sub BGW_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BGW.RunWorkerCompleted
If e.Cancelled = True Then
MsgBox("Cancelado por o utilizador...")
Else
MsgBox("Completei o meu trabalho!" & vbCrLf & e.Result)
End If
End Sub
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
BGW.CancelAsync()
End Sub
End Class
Ideia: Durante a execução do processo demorado, exprimente passar o ponteiro do rato por cima dos butões para confirmar o desimpedimento da UIT. Alternativamente acrescente uma textbox e escreva algo durante a execução do processo demorado.
Nota: O exemplo irá percorrer números desde o 0 (zero) até ao 9999, escrevendo em debug (Debug.Print
) a linha onde se encontra.