VB .NET - Programação Assíncrona com Thread Pools


Quando você esta desenvolvendo uma aplicação pode se deparar com uma situação onde existe a necessidade de realizar um processamento mais intenso que pode fazer com que sua aplicação sofra uma perda de desempenho dando a sensação ao usuário que a aplicação esta '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 esta 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ápido (se a tabela tiver poucos registros) que parece , eu disse parece, que as tarefas são realizadas simultâneamente.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 ter um 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 o formulário 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 esta 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 a parte irei publicar um artigo a respeito em breve.

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

referências:


José Carlos Macoratti