Ir para o conteúdo

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 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.

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.