Tabela de Conteúdos
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 por o 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
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 por a UIT. Neste caso a nossa progressbar. E porque por vezes é necessário cancelar um trabalho, os BackGroundWorkers 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.
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