MultiThreading e CallBacks

Com certeza você, assim como eu e muitos outros, está acostumado a trabalhar
com múltiplas aplicações simultaneamente em seu micro. Deixar um download
acontecendo em background enquato escreve um texto ou deixar o messenger
conectado enquanto escreve um e-mail já viraram tarefas corriqueiras.

Mas como o sistema operacional organiza isso tudo ?

Desde o surgimento do Windows 95 o sistema operacional utiliza um recurso que é
chamado de multitarefa preemptiva. Com a multitarefa preemptiva é o sistema
operacional que divide o tempo de uso de processador entre as aplicações que
estão rodando em nossa máquina, nos dando a impressão de que tudo roda
simultaneamente, o que não é verdade, pois em geral temos apenas um processador.

Cada aplicação, ao ser executada, ganha um espaço de memória isolado para sua
execução, conhecido como processo. Os processos são totalmente isolados uns
dos outros, uma aplicação em um processo não pode ver diretamente outras
aplicações em outros processos.

Mas não é o processo que ganha tempo de execussão do processador. Quem
ganha tempo de execussão do processador são as Threads, unidades de execussão
de código. Cada aplicação tem pelo menos uma thread e o sistema operacional divide
o tempo do processador entre as diversas threads em execussão na máquina.

No VB 6 não podiamos criar múltiplas threads, ficávamos sempre com a única thread
gerada pelo processo. Mas no .NET temos agora muita facilidade em criar múltiplas
threads. A criação de múltiplas threads pela mesma aplicação faz com que a aplicação
ganhe mais tempo de processamento do que as demais, pois haverão mais threads na
mesma aplicação ganhando tempo de processamento.

Outra vantagem do uso de múltiplas threads é o trabalho assincrono. Com uma
única thread, se a nossa aplicação possui alguma tarefa longa para ser realizada
o usuário precisa ficar aguardando a tarefa encerrar para só então continuar o
trabalho, ou seja, a tarefa é realizada de forma síncrona.

Com o uso de multithreading podemos disparar a tarefa em uma thread a parte
enquanto o usuário continua fazendo seu trabalho na thread principal da aplicação.
Quando a tarefa na thread a parte se encerra o usuário é avisado que a tarefa que
ele solicitou já se encerrou.

Essa execução de uma tarefa em uma thread a parte é chamada de execussão
assíncrona, pois o usuário não precisa ficar esperando a tarefa terminar. A tarefa
então precisa avisar a aplicação e, consequentemente, ao usuário, que terminou.
Isso normalmente é feito através de um CallBack : Passa-se como parâmetro para
a thread um ponteiro para uma função de forma que a thread poderá chamar
quando seu trabalho terminar, avisando assim do térmido da tarefa.

Anteriormente isso poderia ser algo muito dificil de criar, mas agora contamos
com a facilidade dos Delegates que funcionam como ponteiros para funções,
nos permitindo exatamente fazer uso do recurso de CallBack.

Então vamos criar uma pequena aplicação, um pequeno exemplo de
uma aplicação multithreading com chamadas assíncronas e CallBacks.
Vamos começar montando
a parte gráfica :

- Crie uma nova Windows Application
- Crie mais um Windows form (form2)
- Insira um botão no Form1 e de o nome de cmdNovaJanela
- No form2 crie um label (estará com o nome de label1)


O Form1 será o responsável por disparar o Form2 e fazer uma contagem sequencial
usando o label do form2. Faremos com que várias cópias do form2 possam ser
disparadas e possam ficar rodando simultaneamente.

Quando o form2 for fechado deverá disparar um callBack para para o Form1
avisando ao Form1 que a tarefa foi completada.

Vamos então criar no Form1 um delegate para que o form2 possa dispara-lo, veja :

Public Delegate Sub Back(ByVal f As Form2)
Public b As Back

O Delegate guarda certa semelhança com a forma de uso de uma classe. O b
foi definido como sendo do tipo da classe e precisaremos criar uma instância do b
, vamos fazer isso no evento Load do form1.

 Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs)
Handles MyBase.Load
     b = New Back(AddressOf Retorno) End Sub


A declaração do Delegate definiu uma assinatura : Uma sub recebendo um
parâmetro do tipo Form2. Ao fazermos o new do delegate passamos o endereço
de uma sub que tenha a mesma assinatura. Em nosso exemplo utilizaremos uma
sub chamada "retorno", criada dentro do form1.

O form2 então precisará de uma variável do tipo "back" para poder receber por
parâmetro o delegate que deverá ser chamado. Iremos também criar uma
variável boleana que servirá para indicar o término do processamento de
contagem que iremos realizar. Teremos então no form2 as seguintes declarações :

Public rodando As Boolean
Public b As Form1.Back


Vamos então fazer uma sub para gerar uma nova instancia do form2 e iniciar o
processamento com esta nova instância. Veja :

 Sub abrirNovoForm()     Dim f As New Form2()     Dim iCNT As Integer     f.Show()     f.b = b     f.rodando=true     Do While f.rodando         f.Label1.Text = iCNT         Application.DoEvents()         iCNT += 1     Loop     f.Dispose()     f = Nothing End Sub

Veja o que esta sub faz :

  • Cria uma instancia do Form2
  • Exibe o Form2
  • Atribui o Delegate ao b do form2
  • Faz um loop com base no atributo rodando que criamos no form2
  • A cada repetição do laço :
    • Preenche o label com o contador
    • Faz um doEventos
    • Aumenta o contador em um
  • Ao término do laço :
    • Destroi o formulário
    • Destroi o ponteiro para o formulário

A idéia então é que está sub seja chamada diversas vezes, mas será executada em
diferentes threads (quem a chamar terá que cuidar disso) para desta forma os f
ormulários poderem ter sua contagem rodando simultaneamente.

A essa altura você deve estar perguntando : Se estamos usando multitarefa
preemptiva, onde o sistema operacional dividirá o tempo de execução entre
as aplicações, então por que chamar o DoEvents em nosso código ?

Ocorre que tanto a sub que descrevemos acima como uma instância do form2
estarão rodando na mesma thread. A sub gera um loop continuo e atualiza o
conteúdo de um label. Porém, devido ao loop continuo, o label não teria tempo
de se redesenhar e não veríamos nada sendo exibido. O que o DoEvents faz
é verificar se existem outras tarefas pendentes NA MESMA THREAD, no caso,
o redesenho do label.

Agora precisamos criar um disparo para esta função. Vamos fazer isso no
clique do nosso botão, no Form1, veja como fica :

 Private Sub cmdNovaJanela_Click
(ByVal sender As System.Object, ByVal e As System.EventArgs)
Handles cmdNovaJanela.Click
     Dim th As New Threading.Thread(AddressOf abrirNovoForm)     th.Start()  End Sub


Apenas essas duas linhas são o bastante. Com a 1a linha criamos uma nova
thread e dizemos que desejamos que esta thread rode a sub "abrirnovoform".
Já na 2a linha fazemos o start da thread, consequentemente da sub, que vai
exibir o form2.

Agora temos que cuidar do encerramento da tarefa. Quando o form2 for
fechado precisaremos disparar o CallBack. Lembra-se que ao abrir o Form2
guardamos o CallBack na variável "b" ?

 Private Sub Form2_Closed(ByVal sender As Object, ByVal e As System.EventArgs)
Handles MyBase.Closed
     b.Invoke(Me) End Sub


Assim sendo no evento closed, que ocorre quando o form2 foi fechado,
fazemos o disparo do CallBack transmitindo a própria instância do form2
como parâmetro. Você se lembra que o Delegate recebe, de acordo com
sua assinatura, uma instância do form2, não é ?

Por fim vamos desenvolver a sub "retorno", que receberá a chamada do
CallBack. Eis a sub :

 Public Sub Retorno(ByVal f As Form2)     MsgBox("O formulário parou em :" & f.Label1.Text)     f.rodando = False End Sub

Observe que não é necessário pararmos a thread. Ao atribuirmos false ao
f paramos o laço que está sendo executado dentro da thread e a thread
será parada naturalmente.

Neste nosso exemplo a sub que abre o form2 faz um loop contínuo, para
demonstrar a atividade em paralelo. E se não tivessemos esse loop ? E se
apenas desejassemos abrir o form em outra thread, mas sem esse
processamento imediato ?

Veja como ficaria uma nova versão desta sub :

     Public Sub abre2()           Dim f As New Form2()            f.b = b           Application.Run(f)     End Sub


Observe a utilização do Application.Run . Toda Thread precisa ter uma
execução contínua, o processador nunca pode estar desocupado. Se
a execução da thread parar a thread é imediatamente eliminada. Então
o application.Run ativa essa execução contínua da Thread, sendo
encarregado inclusive de exibir o formulário f.

No exemplo acima, se não utilizassemos o application.Run, mal veriamos
o formulário, ele seria criado e eliminado imediatamente, devido a parada
de execução da thread. Mas com o application.Run vemos o formulário na
tela e podemos iniciar atividades a partir dele, tudo rodando em uma thread isolada.

Esse é apenas o inicio do trabalho mutithreading. O uso de threads é
algo bem complexo e, dependendo do objetivo, pode exigir um estudo
mais profundo do funcionamento do SO.