Conceitos

Todo programa desenvolvido em C# ou VB.NET possue uma thread. Esta é conhecida como thread principal. Muitos programas geralmente precisam realizar tarefas que levam um longo tempo. Se a thread principal do aplicativo for dedicada a isto, o aplicativo pode parar de responder até que a execução esteja concluída.

Para permitir que um aplicativo execute uma tarefa e continue a responder, ou para realizar várias tarefas ao mesmo tempo, podemos programar conceitos de multitarefas (multithreading).

Além de realizar processamento em segundo plano, é uma ótima maneira de melhorar o desempenho das tarefas do processador em computadores com vários processadores ou núcleos. Se um computador tem vários processadores ou vários núcleos em um único processador, ele pode executar múltiplas tarefas simultaneamente, mesmo que elas exijam tempo de processamento.

Portanto, acrescentar multitarefa em uma aplicação pode reduzir o tempo que leva para completar uma tarefa.

O intuito deste artigo é demonstrar algumas formas de implementar Threads, desde chamadas simples a uma thread, passagem e recebimento de parâmetros, etc.

 

Criando a primeira thread

Existem várias formas de programarmos execução em segundo plano nos softwares que desenvolvemos. Uma delas é criar um método dentro da classe exclusivo para execução da tarefa em segundo plano e outro responsável pela chamada desta tarefa.

// C#
public class GerenciaThread
{
    public void ChamarThread()
    {
        System.Threading.ThreadStart ts =
            new System.Threading.ThreadStart(ExecutarThread);
        System.Threading.Thread t =
            new System.Threading.Thread(ts);
        t.IsBackground = true;
        t.Start();
    }

    private void ExecutarThread()
    {
        // Código a ser executado pela thread
    }
}
' VB.NET
Public Class GerenciaThread
    Public Sub ChamarThread()
        Dim ts As New _
           System.Threading.ThreadStart(AddressOf ExecutarThread)
        Dim t As New System.Threading.Thread(ts)
        t.IsBackground = True
        t.Start()
    End Sub

    Private Sub ExecutarThread()
        ' Código a ser executado pela thread
    End Sub
End Class

 
Repare que no exemplo acima, o único método público dentro da classe ‘GerenciaThread’ é o ‘ChamarThread’. Coloquei desta forma para evitar que o método ‘ExecutarThread’ seja chamado em outros locais a não ser nesta mesma classe, e ,desta forma, permitir que o processo seja executado apenas através da thread em background.

Para iniciar o processamento em segundo plano, basta chamar o método ‘ChamarThread’.

// C#
GerenciaThread gt = new GerenciaThread();
gt.ChamarThread();
' VB.NET
Dim gt As New GerenciaThread()
gt.ChamarThread()

 
NOTA: Para mais informações sobre System.Threading, acesse este endereço.

 

Passando dados para a Thread

Em algumas situações é necessário passar informações para a thread para serem usadas durante o processamento. Essa passagem de dados pode ser feita de várias maneiras. Uma delas é criar um construtor na classe que executa a thread permitindo um ou mais parâmetros. Ao criar a instância da classe, use este construtor e passe os parâmetros desejados. Por exemplo, podemos executar Logs de erros em threads em background. Mas para que o log seja armazenado em algum repositório, é necessário informar para a thread algumas informações como o erro ocorrido, onde ele ocorreu, etc. Usando o mesmo conceito do exemplo anterior, podemos colocar no construtor da classe ‘GerenciaThread’ os parâmetros necessários para gravação do log. Algo parecido com isso:

// C#
public class GerenciaLogErro
{
    private Exception exception;

    // Nome do projeto onde ocorreu o erro
    private string strNomeProjeto;

    // Nome da classe onde ocorreu o erro
    private string strNomeClasse;

    // Nome do método onde ocorreu o erro
    private string strNomeMetodo;

    public GerenciaLogErro(Exception _exception,
        string _projeto,
        string _classe,
        string _metodo)
    {
        this.exception = _exception;
        this.strNomeProjeto = _projeto;
        this.strNomeClasse = _classe;
        this.strNomeMetodo = _metodo;
    }

    public void ExecutarLogErro()
    {
        // Gravar Log no Repositório
        // ou no Log de Eventos do Windows
    }
}

public class LogErro
{
    public void LogarErro(Exception exception,
        string strNomeProjeto,
        string strNomeClasse,
        string strNomeMetodo)
    {
        GerenciaLogErro gle = new
            GerenciaLogErro(exception,
            strNomeProjeto,
            strNomeClasse,
            strNomeMetodo);

        System.Threading.ThreadStart ts = new
            System.Threading.ThreadStart(gle.ExecutarLogErro);
        System.Threading.Thread t = new
            System.Threading.Thread(ts);
        t.IsBackground = true;
        t.Start();
    }
}
' VB.NET
Public Class GerenciaLogErro
    Private exception As Exception

    ' Nome do projeto onde ocorreu o erro
    Private strNomeProjeto As String

    ' Nome da classe onde ocorreu o erro
    Private strNomeClasse As String

    ' Nome do método onde ocorreu o erro
    Private strNomeMetodo As String

    Public Sub New(ByVal _exception As Exception,
        ByVal _projeto As String,
        ByVal _classe As String,
        ByVal _metodo As String)
        Me.exception = _exception
        Me.strNomeProjeto = _projeto
        Me.strNomeClasse = _classe
        Me.strNomeMetodo = _metodo
    End Sub

    Public Sub ExecutarLogErro()
        ' Gravar Log no Repositório
        ' ou no Log de Eventos do Windows
    End Sub
End Class

Public Class LogErro
    Public Sub LogarErro(ByVal exception As Exception,
        ByVal strNomeProjeto As String,
        ByVal strNomeClasse As String,
        ByVal strNomeMetodo As String)
        Dim gle As New  _
            GerenciaLogErro(exception, _
            strNomeProjeto, _
            strNomeClasse, _
            strNomeMetodo)
        Dim ts As New _
            System.Threading.ThreadStart(AddressOf _
            gle.ExecutarLogErro)
        Dim t As New System.Threading.Thread(ts)
        t.IsBackground = True
        t.Start()
    End Sub
End Class

 
Repare que na classe ‘GerenciaLogErro’ foi criado um construtor que recebe 4 parâmetros, e esses parâmetros serão gravados no repositório para registro do Log.

NOTA: Uma prática comum com relação a gravação de Logs é utilizar o EventLog do Windows. Se desejar mais detalhes sobre isso, consulte este e este links.

A chamada para o método que executa o log pode ser feita conforme o código a seguir.

// C#
try
{
    //Coloque aqui o código que deverá ser executado no método
    Console.WriteLine("Esta é a Thread Principal");
}
catch (Exception ex)
{
    LogErro logErro = new LogErro();
    logErro.LogarErro(ex,
        "ThreadPassingData",
        "Program", "Main");
}
' VB.NET
Try
    'Coloque aqui o código que deverá ser executado no método
    Console.WriteLine("Esta é a Thread Principal")
Catch ex As Exception
    Dim logErro As New LogErro()
    logErro.LogarErro(ex, _
        "ThreadPassingData", _
        "Program", "Main")
End Try

 

Recebendo retorno da Thread

Além da passagem de dados para a Thread, existem situações onde é necessário retornar informações quando a execução da thread estiver concluída. Para recuperar dados da thread, devemos criar um método que aceita os resultados de retorno como parâmetro. Em seguida, crie um delegate para este método. O construtor da classe deve aceitar um delegate que representa o método call-back. Antes que a thread esteja concluída, ele chama o delegate de retorno. O código a seguir demonstra isso.

// C#
class Program
{
    static void Main(string[] args)
    {
        // Recebendo entradas do usuário
        Console.WriteLine();
        Console.Write("Informe o primeiro valor: ");
        double d1 = Convert.ToDouble(Console.ReadLine());

        Console.WriteLine();
        Console.Write("Informe o segundo valor: ");
        double d2 = Convert.ToDouble(Console.ReadLine());

        Console.WriteLine();
        Console.WriteLine("OPERAÇÕES");
        Console.WriteLine("1 - Soma");
        Console.WriteLine("2 - Subtração");
        Console.WriteLine("3 - Multiplicação");
        Console.WriteLine("4 - Divisão");
        Console.Write("Informe a operação desejada: ");
        Operacao op =
            (Operacao)Convert.ToInt32(Console.ReadLine());

        // Instância da classe Calculadora
        Calculadora calc = new
            Calculadora(d1, d2, op,
            new ResultDelegate(ResultCallback));

        // Criando a thread
        System.Threading.ThreadStart ts = new
            System.Threading.ThreadStart(calc.Calcular);
        System.Threading.Thread t = new
            System.Threading.Thread(ts);

        // Iniciando a execução da thread
        t.Start();

        Console.WriteLine();
        Console.WriteLine("Thread principal em execução.");
        t.Join();
        Console.WriteLine();
        Console.WriteLine("Processamento concluído.");
        Console.ReadKey();
    }

    public static void ResultCallback(double valor)
    {
        Console.WriteLine("Valor Retornado da Calculadora: {0}",
            valor);
    }
}

public class Calculadora
{
    // Valores utilizados durante o cálculo.
    private double valor1;
    private double valor2;
    private Operacao operacao;

    // Delegate usado para executar o método de
    // call-back quando a thread estiver completa
    private ResultDelegate callback;

    // O construtor obtendo os parâmetros
    public Calculadora(double _valor1,
        double _valor2,
        Operacao _operacao,
        ResultDelegate _callback)
    {
        this.valor1 = _valor1;
        this.valor2 = _valor2;
        this.operacao = _operacao;
        callback = _callback;
    }

    public void Calcular()
    {
        double valorResultado;

        if (this.operacao == Operacao.Soma)
            valorResultado = this.valor1 + this.valor2;
        else if (this.operacao == Operacao.Subtracao)
            valorResultado = this.valor1 - this.valor2;
        else if (this.operacao == Operacao.Multiplicacao)
            valorResultado = this.valor1 * this.valor2;
        else if (this.operacao == Operacao.Divisao)
            valorResultado = this.valor1 / this.valor2;
        else
            valorResultado = 0;

        if (callback != null)
            callback(valorResultado);
    }
}

// Delegate que define a assinatura
//para o método de callback.
public delegate void ResultDelegate(double valor);

// Enumerador de operações
public enum Operacao
{
    Soma = 1,
    Subtracao = 2,
    Multiplicacao = 3,
    Divisao = 4
}
' VB.NET
Sub Main()
    ' Recebendo entradas do usuário
    Console.WriteLine()
    Console.Write("Informe o primeiro valor: ")
    Dim d1 As Double = Convert.ToDouble(Console.ReadLine())

    Console.WriteLine()
    Console.Write("Informe o segundo valor: ")
    Dim d2 As Double = Convert.ToDouble(Console.ReadLine())

    Console.WriteLine()
    Console.WriteLine("OPERAÇÕES")
    Console.WriteLine("1 - Soma")
    Console.WriteLine("2 - Subtração")
    Console.WriteLine("3 - Multiplicação")
    Console.WriteLine("4 - Divisão")
    Console.Write("Informe a operação desejada: ")
    Dim op As Operacao = _
        DirectCast(Convert.ToInt32(Console.ReadLine()), Operacao)

    ' Instância da classe Calculadora
    Dim calc As New _
        Calculadora(d1, d2, op, New _
        ResultDelegate(AddressOf ResultCallback))

    ' Criando a thread
    Dim ts As New _
        System.Threading.ThreadStart(AddressOf calc.Calcular)
    Dim t As New _
        System.Threading.Thread(ts)

    ' Iniciando a execução da thread
    t.Start()

    Console.WriteLine()
    Console.WriteLine("Thread principal em execução.")
    t.Join()
    Console.WriteLine()
    Console.WriteLine("Processamento concluído. ")
    Console.ReadKey()
End Sub

Public Sub ResultCallback(ByVal valor As Double)
    Console.WriteLine("Valor Retornado da Calculadora: {0}", _
        valor)
End Sub

Public Class Calculadora
    ' Valores utilizados durante o cálculo.
    Private valor1 As Double
    Private valor2 As Double
    Private operacao As Operacao

    ' Delegate usado para executar o método de
    ' call-back quando a thread estiver completa
    Private callback As ResultDelegate

    ' O construtor obtendo os parâmetros
    Public Sub New(ByVal _valor1 As Double,
        ByVal _valor2 As Double,
        ByVal _operacao As Operacao,
        ByVal _callback As ResultDelegate)
        Me.valor1 = _valor1
        Me.valor2 = _valor2
        Me.operacao = _operacao
        callback = _callback
    End Sub

    Public Sub Calcular()
        Dim valorResultado As Double

        If Me.operacao = operacao.Soma Then
            valorResultado = Me.valor1 + Me.valor2
        ElseIf Me.operacao = operacao.Subtracao Then
            valorResultado = Me.valor1 - Me.valor2
        ElseIf Me.operacao = operacao.Multiplicacao Then
            valorResultado = Me.valor1 * Me.valor2
        ElseIf Me.operacao = operacao.Divisao Then
            valorResultado = Me.valor1 / Me.valor2
        Else
            valorResultado = 0
        End If

        If (callback <> Nothing) Then
            callback(valorResultado)
        End If
    End Sub
End Class

' Delegate que define a assinatura para o método de callback.
Public Delegate Sub ResultDelegate(ByVal valor As Double)

' Enumerador de operações
Public Enum Operacao
    Soma = 1
    Subtracao = 2
    Multiplicacao = 3
    Divisao = 4
End Enum

 
O resultado desta execução deverá ser algo como isto:

Informe o primeiro valor: 10

Informe o segundo valor: 20

OPERAÇOES
1 – Soma
2 – Subtraçao
3 – Multiplicaçao
4 – Divisao
Informe a operaçao desejada: 2

Thread principal em execuçao.
Valor Retornado da Calculadora: -10

Processamento concluído.

NOTA: Fique sempre atento para o ocorrência de deadlocks. Sempre que necessário, utilize locks nos locais onde a concorrência pode ser grande e causar deadlocks. Isso é assunto para um outro post, mas por enquanto, se quiser saber mais sobre isso, veja este link.

Conclusão

Usar Threads é sempre vantajoso no ponto de vista de que podemos aproveitar o poder de processamento dos processadores que possuem mais de um núcleo, pois podemos programar para que cada thread tenha o seu processamento realizado por núcleos diferentes. Mas é sempre importante avaliar corretamente a utilização de threads pois a concorrência poderá causar deadlocks e funcionamento incorreto de um procedimento.

Estude mais

Escrevi outros artigos sobre este mesmo assunto. Caso queira acessá-los, clique nos links abaixo:

Referências

Anúncios