.NET

2 jul, 2009

Programação Assíncrona com Thread Pools

Publicidade

Quando você está desenvolvendo uma aplicação, pode
se deparar com uma situação onde existe a necessidade de realizar um
processamento mais intenso. Isso pode fazer com que sua aplicação sofra uma perda de
desempenho dando a sensação ao usuário que a aplicação está ‘congelada’.

Se você não precisa aguardar o término deste
processamento para que o usuário interaja com a aplicação, pode usar o recurso do
ThreadPool para obter um comportamento assíncrono sem ter que recorrer ao
processamento de MultiThreads.

MultiThreads??? Meu Deus!!! O que é isto???? Calma!! O
termo MultiThread define a capacidade de se executar múltiplos
processos ao mesmo tempo de forma independente.
Ou seja, você executa duas tarefas (assobiar e chupar cana) ao mesmo
tempo e uma tarefa não depende da outra para ser executada.

Vamos dar um exemplo para clarear: suponha que você queira carregar um controle ListView com os dados de
uma tabela e que, ao mesmo tempo, queira também preencher o conteúdo de uma
Combobox com os dados de outra tabela. Bem, se conseguir realizar tal
proeza ao mesmo tempo e de forma independente, você esta realizando um
processamento MultiThread.

Ora, ora, você pode estar pensando com
os seus botões que o VB6.0 suporta o processamento MultiThread… Mas eu lhe digo que está enganado. Na verdade, quando você
tenta fazer tal tarefa usando o VB 6.0, a tarefa é feita sequencialmente; só
que a coisa é tão rápida (se a tabela tiver poucos registros) que
parece – eu disse parece – que as tarefas são realizadas simultaneamente. O
que o VB6.0 faz é executar múltiplos apartamentos dentro de um único
processo.

No VB.NET a multiTarefa (MultiThread)
é uma realidade, ou seja, podemos ter múltiplos processos paralelos que
podem acessar o mesmo conjunto de dados compartilhados.

A plataforma .NET mantém um pool de
threads prontas para serem usadas, que são idéias para realizar
tarefas rápidas. Geralmente essas threads são usadas para
operações assíncronas de acesso a arquivos ou operações
realizadas pela chamada de um Delegate ou BeginInvoke.

Delegates são tipos usados para invocar um ou mais métodos onde o método atual
invocado é determinado em tempo de execução.

Delegates provê uma
forma de invocar método pelo seu endereço ao invés de seu nome.

Como estas tarefas são rápidas, a
criação e destruição das threads passa a ter uma porção
significativa no tempo de execução da tarefa. Para evitar que
gerenciamento de linhas de execução (Threads) afete o
desempenho, a .NET Framework cria um pool de threads quando
necessário até um valor limite e, então, mantém as threads do
pool em estado de espera e prontas para a próxima operação
assíncrona. Com isso, é ocupada apenas uma pequena quantidade de
memória para cada thread, otimizando o desempenho.

A classe
System.Threading.ThreadPool fornece um número de métodos
estáticos que permitem que você monitore e controle as threads
do pool.

Vejamos os mais importantes:

  • GetMaxThreads – Retorna o número
    máximo das threads ativas no pool.
  • GetAvailableThreads- Retorna a
    diferença entre o número máximo de threads do pool e o número
    atual de threads ativas.
  • GetMinThreads – Retorna o número
    de threads ociosas que o pool mantém e espera de novas
    requisições.
  • SetMinThreads – Altera o valor
    mínimo de threads disponíveis no pool. Se você diminuir
    muito o número de threads ociosas pode afetar o desempenho do
    sistema;
  • SetMaxThreads – Altera o valor
    máximo de threads disponíveis no pool;
  • QueueUserWorkItem – Enfileira um
    método para execução. O método será executado quando uma
    thread do pool estiver disponível. Geralmente você usa este
    método para executar um processo em outra thread. Pode ser usado
    da seguinte forma:
QueueUserWorkItem(WaitCallBack) Enfileira um método para
execução onde o método executa quando a thread estiver
ativa.
QueueUserWorkItem(WaitCallBack,
Object)
Enfileira um método para
execução e especifica um objeto contendo os dados para
ser usado pelo método. O método executa quando a thread
estiver disponível.

Para exemplificar a utilização destes métodos, vamos
usar o Visual Basic 2008 Express Edition para criar
uma aplicação do tipo Console com o nome de
threadPool_1 digitando o código abaixo:

Imports System.Threading
Module Module1

Sub Main()
' exibe o estado padrão das threads do pool
exibeEstadoThreads() '

' Altera os parâmetros do Pool de threads
Console.WriteLine("Alterando o numero de Threads...")
If Not ThreadPool.SetMaxThreads(100, 500) Then
Console.WriteLine("Chamada a SetMaxThreads falhou....")
End If
If Not ThreadPool.SetMinThreads(25, 25) Then
Console.WriteLine("Chamada a SetMinThreads falhou....")
End If

' Inicia uma thread do pool
ThreadPool.QueueUserWorkItem(AddressOf NaofazNada)
Thread.Sleep(10)

' Mostra o novo estado da thread
exibeEstadoThreads()

' Aguarda para encerrar o programa
Console.WriteLine()
Console.WriteLine("Pressione Enter para encerrar...")
Console.ReadLine()

End Sub

Sub exibeEstadoThreads()
' Retorna o número máximo de threads do pool
Dim threadsAtivas As Integer
Dim threadsTerminadas As Integer
ThreadPool.GetMaxThreads(threadsAtivas, threadsTerminadas)

Console.WriteLine("Máximo de threads ativas={0}" & vbCrLf &
"Máximo de I/O threads={1}", threadsAtivas, threadsTerminadas)

' Retorna o no mínimo de threads ociosas
ThreadPool.GetMinThreads(threadsAtivas, threadsTerminadas)

Console.WriteLine("Minino de threads ativas={0}" & vbCrLf &
"Minimo de I/O threads={1}", threadsAtivas, threadsTerminadas)

' Mostra threads disponíveis
ThreadPool.GetAvailableThreads(threadsAtivas, threadsTerminadas)

Console.WriteLine("Threads ativas disponíveis={0}" & vbCrLf &
"Threads I/O disponíveis={1}", threadsAtivas, threadsTerminadas)

End Sub

Sub NaofazNada(ByVal state As Object)
Thread.Sleep(1000)
Console.WriteLine("Sem fazer nada...")
End Sub
End Module

Executando este projeto, iremos obter o seguinte
resultado:

Usando ThreadPool em
uma aplicação Windows Forms

Como já foi mencionado, a classe ThreadPool do namespace
System.Threading contém um pool virtual de threads disponíveis que você pode
usar. Você pode usar a classe ThreadPool para realizar operações assíncronas com
recurso de Multithread sem ter que escrever código para gerenciar o pool de
threads.

A classe ThreadPool gerencia um grupo
dinâmico de threads que estão disponíveis para realizar o processamento
multithread. O número de threads no pool é dinâmico
e você não é obrigado realizar nenhum trabalho extra para gerenciar as threads
no pool. Se você requisitar uma thread e não existir nenhuma disponível, o
pool pode criar uma nova thread ou esperar até que uma thread esteja disponível.

O gerenciamento das threads é feita pelo pool de
forma transparente e você não tem que se preocupar com esse detalhe apenas com o
seu código.

Para demonstrar como usar o recurso ThreadPool
e obter o mesmo resultado obtido pela construção de threads vamos propor um
problema que simula uma aplicação Windows com um grande processamento inicial.
Para forçar uma simulação, a aplicação Windows irá carregar 100 milhões de números

em um controle ListBox.

Se a aplicação simplesmente tentar carregar os
números inteiros no controle ListBox no evento Load do formulário, ele irá demorar vários minutos para ser exibido. Mas se executarmos esta
operação em uma thread separada, então o nosso formulário irá exibir o controle
ListBox de imediato.

Nota: Observe que podemos
usar este recurso pois não precisamos que o processamento de carregar os números
na LIstBox esteja concluído para poder exibir a
ListBox e aguardar a interação
do usuário.

Usando o recurso ThreadPool, o formulário será
carregado de imediato enquanto a LIstBox continua a ser carregada em segundo
plano ficando disponível para a interação com o usuário.

Abra o Visual Basix 2008
Express Edition e crie um novo projeto do tipo
Windows Forms Application com o nome threadPool_2
;

Inclua no formulário padrão
form1.vb  um controle ListBox e um botão de comando Button
conforme o leiaute abaixo:

Defina o namespace Imports
System.Threading para termos acesso a classe ThreadPool.

No evento Load do formulário vamos incluir o
código:

Private  Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles  MyBase.Load

ListBox1.Items.Clear()
ThreadPool.QueueUserWorkItem(AddressOf carregaLista)

End Sub

Estamos usando o método QueueUserWorkItem da
classe ThreadPool para enfileirar a execução da rotina carregaLista
quando uma thread do pool estiver disponível (este método é estático
– Shared – e por isso não precisamos criar uma instância da classe ThreadPool).

Vejamos agora o código da rotina carregaLista():

Private Sub carregaLista(ByVal state As Object)



Dim i As Double

SyncLock ListBox1.GetType

For i = 10000000000000000 To 1 Step -1

item = i

ListBox1.Invoke(CType(AddressOf incluir, MethodInvoker))

Next

End SyncLock

End Sub

Esta rotina irá efetivamente carregar o controle
ListBox com milhões de números. Observe que o código está sendo executado no
interior de um SyncLock.

A instrução

SyncLock  garante que múltiplas
threads não executem as instruções do bloco ao mesmo tempo e previne que
cada thread inicie a execução do código no bloco até que a outra
thread terminou de executar o mesmo o código.

Resta agora exibir o código da rotina incluir
:

	Private Sub incluir()

ListBox1.Items.Add(item)
Application.DoEvents()

End Sub

Neste código estamos incluindo os valores no
controle ListBox.


O método Application.DoEvents permite que seu
aplicativo manipule outros eventos que podem ser disparados enquanto seu código
é executado.

O método
My.Application.DoEvents possui o mesmo
comportamento que o método DoEvents.

Executando o projeto teremos a exibição do
formulário com o controle ListBox já preenchido e a rotina para preenchimento
continua sendo executada em outra thread.

Observe que usamos a thread a partir do pool
através da classe ThreadPool de forma mais simples e sem preocupação em
gerenciar threads.

Uma palavra final sobre Threads em
aplicações ASP .NET. Com certeza podemos também usar o recurso das 
threads nas aplicações ASP .NET para por exemplo executar processos que
envolvem grandes processamentos no servidor. Como o assunto merece um tratamento à parte irei publicar um artigo a respeito em breve.

Eu sei, é apenas VB .NET, mas eu
gosto…