Multithreading com o VB.NET
Um introdução simples e divertida à criação de programas multithread
O Visual Basic .Net abriu novos horizontes para os programadores Visual Basic, e provavelmente a funcionalidade mais interessante seja a capcidade de criação de múltiplas threads, de maneira simples.
Para entender o que é uma thread, primeiro precisamos compreender o que é um processo. De acordo com o site para desenvolvedores da Microsoft, (MSDN - http://www.msdn.microsoft.com/), um processo é “uma instância única de uma aplicação que está sendo executada. Cada processo tem pelo menos uma thread primária, que executa o código dentro do processo. Podem ser criadas threads adicionais em um processo, limitadas apenas pela memória RAM.” Em outras palavras, um processo é um programa executável carregado em memória, e uma thread é a forma utilizada pelo sistema operacional para executar o código do programa.
A MSDN ainda diz que “uma thread executa código em um processo. Cada thread possui sua pilha de execução (espaço usado para controle de chamadas a funções e valores de variáveis). Threads dentro de um mesmo processo compartilham o mesmo espaço de endereçamento. Uma thread também possui registradores associados a ela, sendo o registrador mais conhecido o ‘ponteiro de instruções’. O conjunto de registradores de uma thread é conhecido como ‘contexto’”. Em termos menos técnicos, podemos dizer que uma thread é uma unidade de execução dentro de um processo, compreendendo um ponteiro de instruções (que indica ao sistema operacional qual a próxima instrução a ser executada) e alguma informação de contexto específica da thread.
O relacionamento entre processos e threads fica claro na representação gráfica apresentada na Figura 1.

Figura 1: Processos e Threads
Se um processo tem apenas a thread primária, ele é chamado de single-threaded. Se ele possui mais de uma thread, é chamado de multithreaded. Em programas com múltiplas threads ativas, várias porções de código estarão sendo executadas ao mesmo tempo.
Criando Múltiplas Threads em um Programa
O Visual Basic .Net permite que criemos aplicações multithreaded através da declaração de um objeto do tipo Thread, que é associado a uma função específica, quando a função termina sua execução, a thread é destruída. Os principais métodos para controlar a execução de uma thread são: Start, Stop, Suspend, Sleep(<tempo>), Resume e Abort.
O seguinte exemplo de código mostra um programa simples que cria duas threads adicionais (além da thread primária). Devemos lembrar que a thread primária é sempre criada automaticamente para cada programa.
Imports System.Threading
Private SegundaThread As Thread
Private TerceiraThread As Thread
Sub Main()
SegundaThread =New Thread(AddressOf ThreadCode)
SegundaThread.Name ="Segunda Thread"
SegundaThread.Start()
TerceiraThread =New Thread(AddressOf ThreadCode)
TerceiraThread.Name ="Terceira Thread"
TerceiraThread.Start()
End Sub
Public Sub ThreadCode()
Do while MessageBox.Show("Deseja finalizar esta thread?",_
"Chamado de dentro da Thread " & Thread.CurrentThread.Name,_
MessageBoxButtons.YesNo,MessageBoxIcon.Question) = vbNo
loop
End Sub
Com multithreading, podemos fazer nossos programas executarem diferentes partes do código ao mesmo tempo. Isto pode ser muito útil, por exemplo, se desejamos deixar parte do programa realizando um cálculo de contabilidade complexo para emissão de um relatório, enquanto o usuário continua utilizando outras partes do programa, sem interrupção.
É claro que não podemos criar novas threads em número indefinido. Além da limitação de memória RAM, estabelecida na definição da MSDN, existe ainda um limite prático de threads que podemos criar, da mesma maneira que existe um limite prático para a quantidade de programas que podemos executar em paralelo no sistema operacional antes que ele fique lento demais para ser usado. Este limite irá depender largamente da capacidade da máquina, mas usualmente não devemos criar mais que 10 threads para um mesmo processo.
Evitando erros em programas com Múltiplas Threads
Quando começamos a criar múltiplas threads em nossos programas, devemos ser particularmente cuidadosos com as assim chamadas “seções críticas” de nosso código, ou seja, as partes de nosso programa que podem estar sendo executadas por mais de uma thread ao mesmo tempo e que modificam variáveis globais, gerando resultados inesperados.
Apenas para ilustrar este problema com um exemplo simples, considere a seqüência de pseudo-código abaixo, que mostra a execução de duas threads cujas tarefas são executadas de forma sequencial:
1. Thread 1 g a variável. Seu valor é 10.
2. Thread 1 incrementa a variável (novo valor da variável: 11).
3. Thread 1 salva o valor (valor salvo: 11).
4. Thread 2 lê a variável. Seu valor é 11.
5. Thread 2 incrementa a variável (novo valor da variável: 12).
6. Thread 2 salva o valor (valor salvo: 12).
Considere agora este mesmo exemplo, onde a execução não ocorre de maneira seqüencial, conforme apresentado a seguir. Veja que a seqüência de execução abaixo é perfeitamente válida (embora gere um erro na atualização da variável), pois cada ponteiro de instruções garante o seqüenciamento das atividades dentro da thread, mas não entre diferentes threads.
1. Thread 1 lê a variável. Seu valor é 10.
2. Thread 1 incrementa a variável (novo valor da variável: 11).
3. Thread 2 lê a variável. Seu valor é 10.
4. Thread 1 salva o valor (valor salvo: 11).
5. Thread 2 incrementa a variável (novo valor da variável: 11).
6. Thread 2 salva o valor (valor salvo: 11).
Este tipo de erro pode ser desastroso quando estamos acessando recursos que não são thread-safe, como determinadas DLLs, e pode levar a instabilidade no programa. Para evitar este tipo de problemas, o VB .Net oferece um novo bloco de comandos que ajuda a proteger seções com dados críticos.
O bloco SyncLock()/End SyncLock() informa ao Visual Basic .Net que qualquer dado dentro do bloco só pode ser acessado por uma thread de cada vez. O comando SyncLock() recebe como parâmetro uma referência a um objeto (usualmente, um objeto de uma classe criada pelo programa, mas também pode ser um módulo, um array ou uma janela, por exemplo), e evita que a thread execute o código do bloco até que ela consiga um lock de acesso exclusivo ao objeto referenciado pelo parâmetro. Por exemplo, SyncLock(Me) trava o objeto corrente.
O exemplo de código a seguir mostra como uma variável global pode ser protegida para que se evitem problemas de threads concorrentes.
Imports System.Threading
Private MinhaVariavelGlobal as integer =0
Private SegundaThread As Thread
Private TerceiraThread As Thread
Sub Main()
SegundaThread =New Thread(AddressOf SegundaThreadCode)
SegundaThread.Name ="Segunda Thread"
SegundaThread.Start()
TerceiraThread =New Thread(AddressOf TerceiraThreadCode)
TerceiraThread.Name ="Terceira Thread"
TerceiraThread.Start()
End Sub
Public Sub SegundaThreadCode()
Synclock(me)
MinhaVariavelGlobal +=_
inputbox ("Entre com o valor a somar na variável global",_
Thread.CurrentThread.Name )
End Synclock
End Sub
Public Sub TerceiraThreadCode()
Synclock(me)
MinhaVariavelGlobal +=_
inputbox ("Entre com o valor a somar na variável global",_
Thread.CurrentThread.Name )
End Synclock
End Sub
No código acima, nós protegemos a variável global contra a perda de valores, mas o que faríamos caso quiséssemos saber o valor final da variável, após ambas as threads terem terminado? O método Join do objeto Thread nos ajuda a escrever um programa que espere uma thread terminar, tornando possível a criação de rotinas de sincronização. Ele força a thread que o invoca a entrar em estado de espera até que a thread de destino termine.
Adicionando as seguintes linhas ao fim da função Main do programa apresentado no exemplo anterior, podemos forçar o programa principal a esperar que ambas as threads terminem sua execução, e só então continue seu fluxo normal, apresentando o valor final da variável global, como desejado.
SegundaThread.join()
Terceirathread.join()
MessageBox.show("Valor final da variável Global:"&_
MinhaVariavelGlobal.ToString)
Utilizando Multithreading em um exemplo divertido
De maneira a ilustrar o conceitos de múltiplas threads executando em um mesmo programa, iremos a seguir criar um jogo simples, onde nosso objetivo é limpar o computador de um ataque de supostos vírus. Este jogo, batizado de D-iNfEcT (notem as letras maiúsculas...) é o mais simples jogo criado em no livro “.Net Game Programming with DirectX 9.0”, recentemente publicado, que apresenta diversos jogos com uso de Visual Basic .Net, GDI+ e Managed DirectX 9.0.
O jogo será composto de uma janela principal, que deverá ter uma forma irregular (simulando o formato de uma bactéria), com opções para definir o número de germes filhos a serem gerados, e o tempo para capturar estes filhos.
Uma vez iniciado o jogo, a janela principal irá gerar novas threads, que por sua vez irão apresentar janelas em formato de germe que se movimentarão de maneira errática na tela do computador. O jogador deverá clicar nestas janelas-filha, o que fará com que elas terminem a execução da thread associada. Caso o tempo para pegar todos os germes termine, o programa apresentará uma janela de mensagem indicando que o computador está irreversivelmente contaminado; e caso o jogador consiga capturar todos os germes a tempo, uma mensagem de congratulação será apresentada.
Usaremos neste jogo conceitos explorados em dois outros artigos desta revista: a criação de janelas não-retangulares; e o acesso a código não gerenciado, para emitir um som cada vez que um germe for “capturado” com um clique de mouse.
Uma “tela” típica do jogo pode ser vista na Figura 2

Figura 2: Jogando D-iNfEcT
Destacaremos aqui apenas as partes relacionadas ao controle das threads, o código completo pode ser acessado no site da editora, em http://xxxxxxxxxxxxxx[ASLo1]
Organização do código
A lógica do jogo estará concentrada nos eventos da janela principal e das threads das janelas filhas, conforme descrito nas tabelas a seguir:
Tabela 1: Eventos da Janela Principal
|
Evento |
Ação |
|
Load |
Modificar o formato da janela |
|
Botão iNfEcT! |
Criar threads conforme o definido pelo jogador, e iniciar o timer para contagem regressiva |
|
Timer |
Decrementar o contador de tempo, se ele chegar a zero, mostrar a mensagem de game over |
|
Botão Fechar |
Encerrar todas as threads criadas pelo programa |
Tabela 2: Eventos das Janelas Filhas
|
Evento |
Ação |
|
Load |
Modificar o formato da janela |
|
Clique de mouse |
Fechar a janela, decrementar a variável de “germes ativos” na janela principal, e tocar um som usando a função de DLL SndPlaySound, conforme descrito no artigo sobre acesso a código não gerenciado. |
|
Timer |
Escolher uma direção e mover a janela |
Não estaremos aqui mostrando o código que modifica o formato das janelas, que segue as idéias discutidas no artigo sobre criação de janelas não retangulares nesta revista; nem a função PlaySound, que executa a chamada ao código não gerenciado conforme descrito no artigo sobre este assunto.
Codificando a janela principal
Para que a janela principal já seja apresentada no formato de um germe, deveremos chamar a função que modifica a forma da janela no evento Load.
Sub frmStart_Load(sender As Object, e As EventArgs) _
Handles MyBase.Load
ModificarFormaDaJanela(Me, 4)
End Sub
A função ModificarFormaDaJanela recebe um inteiro que indica o tamanho da janela, e uma referência à janela a ser modificada, e será usada também pelas janelas filha. Consulte o código disponível no site da editora para ver os detalhes desta função.
Quando trabalhando com threads, devemos ser cuidadosos não só na sua criação, mas também na destruição, pois enquanto uma thread estiver ativa, nosso programa continua em memória – mesmo que a janela e a thread primária já tenham encerrado!
Para controlar as threads criadas, definiremos um array com referências para cada thread, que pode ser utilizado para fechar todas as threads ativas quando necessário.
Para criar as threads, iremos simplesmente definir uma função que será passada como argumento para o método New do objeto Thread. Por enquanto, esta função deverá apenas apresentar uma janela filha de maneira modal, de forma que a thread é finalizada quando a janela é fechada.
O código para o botão iNfEcT! e a função usada para a criação de threads é apresentada no exemplo a seguir:
Private GemThread As Thread()
Sub cmdGO_Click(sender As Object,e As EventArgs) Handles cmdGO.Click
Dim i As Integer
ReDim GemThread(NumGerms.Value)
' Cria todos os germes / threads, conforme definido na
' janela principal
For i =0 To NumGerms.Value -1
GemThread(i)=New Thread(AddressOf NovoGerme)
GemThread(i).Name ="Germe " & (i +1)
GemThread(i).Start()
Next
' Habilita o timer que realiza a contagem regressiva
TmrTime.Enabled =True
End Sub
Sub NovoGerme()
Dim f As New FrmGerm()
' Mostra a janela de maneira modal
f.ShowDialog()
' Se o código continua executando neste ponto, é porque a janela
' foi fechada, ou seja, o germe foi capturado
End Sub
Se rodarmos nosso programa agora, poderemos ver que ele cria diversas janelas filhas, conforme o definido pelo jogador no controle updown de nome NumGerms, definido na interface da janela principal.
Se fecharmos a janela principal, veremos que as janelas filhas ainda continuam executando. Para evitar este problema, devemos explicitamente abortar todas as threads no evento Closing da janela principal, de maneira a garantir que todas as threads são encerradas adequadamente – mesmo que a janela seja encerrada via comando ALT+F4. No próximo trecho de código mostramos a rotina que aborta todas as threads (ignorando eventuais erros que podem acontecer caso a thread não exista), bem como o código a ser incluído no botão de fechar a janela e no evento de fechamento.
Sub KillGerms()
Dim i As Integer
For i =0 To NumGerms.Value -1
Try
GemThread(i).Abort()
Catch
' Ignora erros caso a thread não exista
End Tr
Next
End Sub
Private Sub cmdClose_Click(sender As Object,e As EventArgs)_
Handles cmdClose.Click
Me.Close()
End Sub
Private Sub frmStart_Closing(sender As Object,e As CancelEventArgs)_
Handles MyBase.Closing
KillGerms()
End Sub
Com este código, nossa janela principal já funcionará adequadamente para a criação e destruição das threads. De maneira a facilitar o entendimento sobre o mecanismo das threads, vamos analisar agora o código das janelas filhas, incluindo ao fim do artigo as condições de término do jogo.
Codificando as janelas filhas (germes)
As janelas filhas em nosso jogo serão bastante simples: tudo o que precisamos fazer é modificar a forma da janela quando ela for aberta (evento Load), escolhendo uma posição aleatória na tela para a criação da janela; depois movimentar a janela no evento Elapsed do objeto Timer, e codificar o evento de Click do formulário para fechar a janela caso o usuário clique na janela.
Inicialmente, vamos incluir apenas o código para modificar o formato da janela e fechá-la quando o jogador clicar com o mouse; assim, poderemos testar mais facilmente o programa. O código para os eventos de Load e Click é apresentado na próxima listagem.
Sub Form_Load(sender AsObject,e As EventArgs) Handles MyBase.Load
Dim x As Integer
Dim y As Integer
' Modifica a forma da janela para parecer um germe pequeno
ModificarFormaDaJanela(Me,0.3)
' Escolhe uma posição aleatória na tela
Randomize()
x =Rnd()*Screen.PrimaryScreen.WorkingArea.Width
y =Rnd()*Screen.PrimaryScreen.WorkingArea.Height
Me.Location =New Point(x,y)
End Sub
Sub Form_Click(sender As Object,e As EventArgs) Handles MyBase.Click
' Se o jogador clicou na janela, o germe morre (janela fecha)
Me.Close()
End Sub
Se rodarmos o programa agora, seremos capazes de pressionar o botão iNfEcT! e ver o resultado das novas threads criadas: diversas novas janelas aparecerão em posições variadas da tela.
Podemos ainda “matar” cada uma das janelas-germe clicando sobre elas – o que não é muito desafiador, pois além delas estarem paradas, nosso contador de tempo na janela principal ainda não está ativo.
Completamos o código das janelas filhas codificando o evento Elapsed do objeto Timer, conforme apresentado na próxima listagem. No código a seguir, criamos uma constante chamada aleatoriedade, que indicará o grau de aleatoriedade do movimento das janelas-filha: quanto maior este valor, mais aleatoriamente as janelas se moverão. Se colocarmos um valor igual a 0, as janelas irão se mover linearmente pela janela, modificando sua posição apenas quando atingirem as bordas da tela.
Analise o código e leia os comentários no código a seguir para verificar a forma como é produzido o movimento das janelas.
Const aleatoriedade As Single =0.05
Sub Timer1_Elapsed(sender As Object,e As ElapsedEventArgs) _
Handles Timer.Elapsed
Dim x As Integer = Me.Location.X
Dim y As Integer = Me.Location.Y
Static incX As Integer =10, incY As Integer =10
' Sorteia um valor e modifica a direção se ele for
' menor que a constante 'aleatoriedade'
If Rnd(1) < aleatoriedade Then incX = -incX
If Rnd(1) < aleatoriedade Then incY = -incY
' Se a janela vai se mover para fora da tela, inverte sua direção
If x + incX > Screen.PrimaryScreen.WorkingArea.Width Then incX = -incX
If y + incY > Screen.PrimaryScreen.WorkingArea.Height Then incY =-incY
If x + incX < 0 Then incX = -incX
If y + incY < 0 Then incY = -incY
' Atualiza posição da janela na tela
x += incX
y += incY
Me.Location = New Point(x,y)
End Sub
Executando nosso programa mais uma vez, podemos ver os novos germes se movimentando livremente, sem sair da tela. Experimente diferentes valores para a constante de aleatoriedade e verifique os resultados!
Adicionando as condições de Fim de Jogo
Para este jogo, teremos duas condições de finalização: O jogador captura todos os germes, de forma a ganhar o jogo, ou o tempo termina e o jogador perde o jogo.
Para controlar o tempo de jogo, basta adicionar um timer à janela principal e configurar o intervalo de tempo para 1 segundo. A cada evento do timer, decrementamos o tempo o contador de tempo restante (que pode ser diretamente o valor mostrado na tela pelo label de nome lblTImeLeft), e terminamos o jogo quando o valor do tempo for zero. O trecho de código a seguir apresenta a lógica do controle de tempo da janela principal, incluindo a mensagem de “Fim de Jogo” ao fim do tempo determinado.
Sub TmrTime_Elapsed(sender As Object,e As EventArgs) _
Handles TmrTime. Elapsed
' Decrementa o tempo
lblTimeLeft.Text -= 1
' Checa se o tempo acabou
If lblTimeLeft.Text = 0 Then
' Aborta todas as threads porventura ativas
TmrTime.Enabled = False
KillGerms()
MsgBox("Seu computador não pode ser mais curado!", _
MsgBoxStyle.Critical,"O tempo acabou!")
End If
End Sub
Para controlar o número de germes ainda vivos e declarar vitória caso o jogador consiga capturar todos os germes, iremos modificar a função utilizada para gerar as threads, incluindo código para decrementar o contador de germes ainda vivos (diretamente o label da tela, de nome lblGermsLeft), e mostrar a mensagem de sucesso quando o contador chegar a zero.
É importante observar que, mesmo que seja improvável que o jogador clique em duas janelas ao mesmo tempo, é importante proteger a área crítica do nosso jogo; neste caso, o label que controla o número de germes vivos nunca deve ser acessado por duas threads ao mesmo tempo, ou poderemos ter resultados imprevistos. Como já visto anteriormente, basta incluir um bloco SyncLock / End SyncLock para garantir que não teremos problemas de conflitos com a concorrência entre threads.
O exemplo de código a seguir mostra a versão final do
To control the number of active germs, we can add extra code to our NovoGerme
function, which handles the germ window creation, to decrement the remaining
germs label and perform the corresponding action when the last germ is caught.
The full code for the function is presented in the next code sample:
Public Sub NovoGerme()
Dim f As New FrmGerm()
' Mostra a janela de maneira modal
f.ShowDialog()
' Se o código continua executando neste ponto, é porque a janela
' foi fechada, ou seja, o germe foi capturado
SyncLock (Me)
' Decrementa o número de germes vivos
lblGermsLeft.Text -=1
' Verifica se matamos o último germe
If lblGermsLeft.Text =0 Then
MessageBox.Show("Você salvou seu computador!! " & _
"Os vírus se foram!", "Parabéns!", MessageBoxButtons.OK,_
MessageBoxIcon.Exclamation)
TmrTime.Enabled =False
End If
End SyncLock
End Sub
Como um toque adicional, podemos tocar um arquivo de som toda vez que um germe for capturado, chamando a função sndPlaySoundA, da biblioteca de ligação dinâmica do Windows WinMM.DLL. Veja nesta revista o artigo sobre acesso a código não gerenciado para verificar como incluir esta chamada, ao fim da função NovoGerme.
Com este último detalhe, nosso jogo (e primeiro programa que usa funcionalidades de multithreading no Visual Basic .Net) está completo!
Divirtam-se incrementando o jogo com novas funcionalidades (como, por exemplo, modificar dinamicamente a cor ou forma dos germes para dar-lhes uma aparência mais “viva”), e aproveite os conceitos de multithreading para surpreender seu chefe, fazendo com que aquele seu velho programa que demora 30 minutos para gerar um relatório possa calcular e emitir o relatório “em background”, enquanto o feliz usuário poderá continuar com suas tarefas normais!
[ASLo1]Inserir aqui endereço do site da editora, onde estará o código para download
