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