VB .NET 2008 – Threading Básico: Teoria, Dicas e Armadilhas


 Hoje em dia, temos a necessidade de tornar nossas aplicações dinâmicas, rápidas e interativas, afinal estamos na era dos Dual/Quad Cores, o que antes era resumido em alguns poucos megabytes de memória RAM hoje são gigabytes.

Realmente, os microcomputadores estão super velozes, mas isto não significa que podemos desperdiçar processamento e/ou memória ram. Não digo que toda rotina deve ser minuciosamente otimizada mesmo porque isto toma tempo e nem sempre temos o tempo como nosso aliado – na maioria das vezes é nosso grande inimigo.

 Vamos ao que importa, Threads. Antes de mais nada, Threading é um assunto bem complexo, com certeza não sou perito no assunto mas acredito que de forma simples e com exemplos práticos poderei compartilhar um pouco do conhecimento com os leitores.

Sendo assim, caso eu cometa algum equívoco ou até mesmo erro conceitual, não hesite em me contatar para correção!

O que é uma Thread?

Thread (ou linha de execução) é uma forma de um determinado programa criar uma ou mais tarefas que podem ser executadas simultaneamente. A maioria dos programas que utilizamos no nosso dia a dia utilizam Threads para fazer outras ações sem que este programa trave.

Vamos a um exemplo bem básico?

Um download! Independente de seu navegador, seja o Internet Explorer, Firefox, Chrome ou qualquer outro você pode fazer um download, ver sua barra de progresso aumentar, e ainda assim continuar navegando sem nenhum problema, e uma ou mais threads estão por trás disso.

Enquanto uma tarefa (permitir a navegação) está sendo executada pelo usuário – outra tarefa (fazer o download do arquivo) está sendo executada pelo navegador, parece simples mas não é.

Outro exemplo bem legal e simples de se entender é a barra de progresso, não que ela não possa ser executada em conjunto, mas vamos supor que você está desenvolvendo um programa para gerenciar pessoas de uma cidade, temos 400.000 pessoas registradas e por algum motivo qualquer você quer listar os 400.000 registros de uma só vez.

Enquanto seu DataTable (ou outro objeto de sua escolha) está sendo preenchido, você não pode fazer nada no seu programa.

Criando uma thread você pode atualizar seu DataTable, mostrar uma barra de progresso e informar ao usuário quantos registros ainda faltam, tudo isso sem o usuário ficar na famosa “tela branca”/”não respondendo”.

Threading na Prática

 Agora que você ja sabe o que é e para que serve uma thread, vamos praticar com situações reais.

Atualização da UI via Thread

Fiquei impressionado com a quantidade de pessoas com o mesmo problema. Vamos supor que estamos importando um banco de dados qualquer para um DataTable. Enquanto isto ocorre, não queremos que o usuário fique impedido de fazer seus trabalhos ou principalmente, não queremos a tela de “Não respondendo”.

Na thread principal temos o um form criado com um GridView, uma progressbar e uma label indicando quantos registros temos, e quantos já foram lidos. Pode parecer simples: ler o banco, preencher o DataTable, e ir atualizando a progress bar e as labels – engano seu!

Se fizermos desta forma, bloquearemos qualquer ação do usuário, a tela provavelmente vai ficar branca com o titulo de “Não Respondendo” dependendo da quantidade de registros. A solução é criar um processo separado para preencher o DataTable e atualizar a parte gráfica que é a interação com o usuário.

Ai temos outro problema: o form onde está a parte gráfica está em uma thread e a parte que executa a tarefa de importação está em outra thread. Em outras palavras eu não posso (ou pelo menos não deveria) fazer isto:

LabelQuantidade.Text = “10000”

O programa provavelmente irá travar com uma exception muito conhecida pelas pessoas que trabalham com threads: IllegalCrossThreadException. O que ocorre é que uma Thread não pode mandar diretamente nos objetos de outra Thread, mesmo pertencendo ao mesmo programa. Mas por que isto acontece?

Vamos adaptar a situação para nosso cotidiano: imagine que temos apenas uma faca na cozinha, seu amigo precisa cortar cebola e você precisa de cortar tomate. Como só temos uma faca, ou você corta o tomate ou seu amigo corta a cebola. Se você estiver cortando tomate e seu amigo resolver tirar a faca da sua mão na força, um dos dois pode cortar o dedo acidentalmente.

 É exatamente isto que ocorre no programa, temos um objeto para duas threads, se uma thread está utilizando um objeto e outra thread tenta utilizá-lo também, o programa trava e dados podem ser perdidos. Para evitar este tipo de comportamento podemos utilizar o MethodInvoker e para que o método de atualização seja executado diretamente pelo form que é dono do objeto? Tá complicado? Vamos ao exemplo!

1.       Crie um novo projeto .NET Framework 3.5 Windows Forms e os itens dispostos da seguinte forma:

 

 

Temos os seguintes objetos:

1-      grdDados: Mostrará os dados de um determinado DataTable

2-      Label1: Total de Registros

3-      lblTotalRegistros: Mostra a quantidade total de linhas do DataTable

4-      Label2: Registros adicionados ao GridView

5-      lblTotalRegistrosAdicionados: Mostra quantas linhas existem no GridView

6-      pbDados: ProgressBar que exibirá o progresso de preenchimento do GridView

7-      btnLerDados: Faz a busca numa tabela do AdventureWorks no SQL Server 2005 Express (desculpem, sem Northwind por aqui).

Para fins de exemplo, não vou fazer toda a camada de acesso a dados nem seguir a risca o padrão MVC, a ideia aqui é mostrar como atualizar os itens do form via thread.

Quando criamos o form, vamos adicionar a tabela Customers ao nosso projeto:


        Passo 1: Selecione Database

Passo 2: Crie uma nova conexão para acessar seu banco e o Banco de Dados AdventureWorks.

Passo 3: Por conveniência, vamos salvar a connectionstring no config.app.

Passo 4: Adicionamos a tabela Customers ao nosso dataset dsCustomers

Feito isto selecione o GridView -> Choose DataSource e marque a tabela Customers.

Repare que 3 objetos foram criados, volte no datagrid e remova o DataSource, vamos fazer isto via código (fizemos este método pois o Visual Studio ja criar os objetos pra você).

O TableAdapter ja possui um metodo Fill que vai cuidar de fazer o SELECT na tabela.

Agora, vamos Importar o Namespace System.Threading que é responsável pelo gerenciamento das Threads.

Imports System.Threading

Feito isto criaremos uma sub-rotina que irá cuidar do serviço sujo e comentar um pouco sobre ela:

Private Sub CustomerThread()

        Try

            If (Me.InvokeRequired = True) Then

                Me.BeginInvoke(New MethodInvoker(AddressOf Me.CustomerThread))

            Else

                Me.CustomerTableAdapter.Fill(Me.DsCustomers.Customer)

                lblTotal.Text = DsCustomers.Tables(0).Rows.Count

                pbDados.Maximum = Integer.Parse(lblTotal.Text)

                pbDados.Value = 0

                For Each dc As DataColumn In DsCustomers.Tables(0).Columns

                    grdDados.Columns.Add(dc.ColumnName, dc.ColumnName)

                Next

                For Each dr As DataRow In DsCustomers.Tables(0).Rows

                    grdDados.Rows.Add(dr.ItemArray)

                    lblTotalAdicionado.Text += 1

                    pbDados.Value += 1

                    Application.DoEvents()

                    Thread.Sleep(100)

                Next

            End If

        Catch ex As ThreadAbortException

 

        End Try

     

    End Sub

If (Me.InvokeRequired = True) Then

Verificamos se precisamos utilizar o invoke.

 

Me.BeginInvoke(New MethodInvoker(AddressOf Me.CustomerThread))

O MethodInvoker é uma delegate, mas não precisamos criar um tipo Delegate para um metodo, bem mais simples. Não sabe o que é delegate?

Leia o artigo do Macoratti aqui.

 

Bom todas as outras partes estão auto explicativas. Esta função não é 100% ThreadSafe, podemos melhorá-la utilizando Locks, mas este é assunto para o próximo artigo!

Nota Final: Utilizamos em nossa subrotina CustomerThread a palavra Me. Isto só pode ser utilizado caso a subrotina fique na mesma classe do form em que ela irá atualizar, caso queremos utilizar uma função de outra classe devemos fazer da seguinte forma:

Dim frmReal As Form1

        frmReal = CType(Application.OpenForms("Form1"), Form1)

        If (frmReal.InvokeRequired = True) Then

            frmReal.BeginInvoke(New MethodInvoker(AddressOf Me.AtualizarPlacar))

 

Os itens também devem ser referenciados como frmReal, pois caso contrario a Thread criara um form “fantasma” e seus objetos não serão atualizados.

                A função CustomerThread ainda não é ThreadSafe mesmo assim, por quê? Pesquise sobre o SyncLock e como ele pode ajuda-lo com este problema.

                No próximo artigo abordarei sobre a sincronização de Threads e também sobre o SyncLock.

Até a próxima!

Alexandre

Exemplo em anexo.:  ´ThreadingExemplo.rar