Sincronização de Threads em C# e VB.NET

1 Comentário

Aviso: Boa parte do conteúdo deste artigo é uma tradução de artigo “Thread Synchronization (C# and Visual Basic)” disponibilizado pela Microsoft, que pode ser encontrado aqui.

Um dos benefícios do uso de múltiplas threads em uma aplicação é que cada uma delas é executado de forma assíncrona. Em aplicações desktop, isto permite que tarefas que consomem muito tempo possam ser executadas em segundo plano enquanto a janela do aplicativo e os controles continuam respondendo. Para aplicações em servidores, múltiplas tarefas fornecem a capacidade de lidar com cada solicitação de entrada com uma thread diferente, caso contrário, cada novo pedido não será atendido até que o pedido anterior tenha completado.

No entanto, a natureza assíncrona de threads significa que o acesso a recursos compartilhados, como arquivos, conexões de rede e memória devem ser sincronizados. Caso contrário, duas ou mais threads podem acessar o mesmo recurso ao mesmo tempo, e cada um desconhece a atuação da outra ação e o resultado é a corrupção de dados imprevisível e possíveis deadlocks.

Há algum tempo, fiz outros artigos que demonstram o uso de threads em C# e Visual Basic. Você pode encontrá-los aqui:

Lock e SyncLock

O Lock (C#) e o SyncLock (VB.NET) são declarações que podem ser utilizadas para garantir que um bloco de código seja executado até sua conclusão, prevenindo a interrupção da execução por outras threads. Essas declarações são interpretadas pelo compilador como bloqueios para um determinado objeto em um bloco de códigos. Se outra thread tenta executar o código envolvido pelo lock / SyncLock, ela esperará até que a primeira thread termine a execução e libere o objeto / código bloqueado.

//C#
public class LockTest
{
    public void TestarLock()
    {
        lock (this)
        {
            // Código que estará protegido pelo lock.
        }
    }
}
'VB.NET
Public Class LockTest
    Public Sub TestarLock()
        SyncLock Me
            ' Código que estará protegido pelo lock.
        End SyncLock
    End Sub
End Class

Sincronização de Eventos e Wait Handles

Existem dois tipos de sincronização: AutoResetEvent e ManualResetEvent. Eles diferem apenas na medida em que as mudanças são setadas automaticamento no AutoResetEvent e manualmente no ManualResetEvent. O ManualResetEvent permite que qualquer número de threads possa ser alinhado e ter seu estado atualizado.

Métodos são usados para esperar a execução de um ou mais threads. Esses métodos indicam ao compilador quando e quantas threads devem ter o processamento concluído para que o fluxo continue. São eles: WaitOne, WaitAny e WaitAll. WaitOne faz com que o compilador espere a execução de um único thread estar concluída. WaitAny faz o compilador aguardar a execução dos métodos indicados. WaitAll bloqueia a execução até que todas as threads estejam concluídas. Para sinalizar a conclusão da execução de uma thread usa-se o método Set.

// C#
namespace GerenciaSincronizacao
{
    public class GerenciarThreads
    {
        private ManualResetEvent[] manualResetEvent;

        private void ExecutarThread0(object obj)
        {
            Thread.Sleep(20000);
            Console.WriteLine("Thread 0 concluída!");
            manualResetEvent[0].Set();
        }

        private void ExecutarThread1(object obj)
        {
            Thread.Sleep(13000);
            Console.WriteLine("Thread 1 concluída!");
            manualResetEvent[1].Set();
        }

        private void ExecutarThread2(object obj)
        {
            Thread.Sleep(9000);
            Console.WriteLine("Thread 2 concluída!");
            manualResetEvent[2].Set();
        }

        private void ExecutarThread3(object obj)
        {
            Thread.Sleep(17000);
            Console.WriteLine("Thread 3 concluída!");
            manualResetEvent[3].Set();
        }

        public void Executar()
        {
            manualResetEvent = new ManualResetEvent[4];

            manualResetEvent[0] = new ManualResetEvent(false);
            ThreadPool.QueueUserWorkItem(this.ExecutarThread0);

            manualResetEvent[1] = new ManualResetEvent(false);
            ThreadPool.QueueUserWorkItem(this.ExecutarThread1);

            manualResetEvent[2] = new ManualResetEvent(false);
            ThreadPool.QueueUserWorkItem(this.ExecutarThread2);

            manualResetEvent[3] = new ManualResetEvent(false);
            ThreadPool.QueueUserWorkItem(this.ExecutarThread3);

            WaitHandle.WaitAll(manualResetEvent);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            GerenciarThreads gt = new GerenciarThreads();
            gt.Executar();

            Console.WriteLine("Pressione ENTER para terminar.");
            Console.ReadKey();
        }
    }
}
' VB.NET
Namespace GerenciaSincronizacao
    Public Class GerenciarThreads
        Private manualResetEvent As ManualResetEvent()

        Private Sub ExecutarThread0(obj As Object)
            Thread.Sleep(20000)
            Console.WriteLine("Thread 0 concluída!")
            manualResetEvent(0).[Set]()
        End Sub

        Private Sub ExecutarThread1(obj As Object)
            Thread.Sleep(13000)
            Console.WriteLine("Thread 1 concluída!")
            manualResetEvent(1).[Set]()
        End Sub

        Private Sub ExecutarThread2(obj As Object)
            Thread.Sleep(9000)
            Console.WriteLine("Thread 2 concluída!")
            manualResetEvent(2).[Set]()
        End Sub

        Private Sub ExecutarThread3(obj As Object)
            Thread.Sleep(17000)
            Console.WriteLine("Thread 3 concluída!")
            manualResetEvent(3).[Set]()
        End Sub

        Public Sub Executar()
            manualResetEvent = New ManualResetEvent(3) {}

            manualResetEvent(0) = New ManualResetEvent(False)
            ThreadPool.QueueUserWorkItem(AddressOf Me.ExecutarThread0)

            manualResetEvent(1) = New ManualResetEvent(False)
            ThreadPool.QueueUserWorkItem(AddressOf Me.ExecutarThread1)

            manualResetEvent(2) = New ManualResetEvent(False)
            ThreadPool.QueueUserWorkItem(AddressOf Me.ExecutarThread2)

            manualResetEvent(3) = New ManualResetEvent(False)
            ThreadPool.QueueUserWorkItem(AddressOf Me.ExecutarThread3)

            WaitHandle.WaitAll(manualResetEvent)
        End Sub
    End Class

    Class Program
        Private Shared Sub Main(args As String())
            Dim gt As New GerenciarThreads()
            gt.Executar()

            Console.WriteLine("Pressione ENTER para terminar.")
            Console.ReadKey()
        End Sub
    End Class
End Namespace

A classe Interlocked

Você pode usar os métodos da classe Interlocked para evitar problemas que podem ocorrer quando várias threads tentam simultaneamente atualizar ou comparar um mesmo valor. Os métodos dessa classe permitem que você assegure o incremento, decremento, troca e comparação entre valores em qualquer thread.

As operações mais utilizadas desta classe são:

  • O método Add adiciona um valor inteiro para uma variável e retorna o novo valor da variável.
  • O método Read lê um valor inteiro de 64 bits.
  • Os métodos Increment e Decrement são utilizados para incrementar ou decrementar valores em uma variável e retornam o valor resultante.

O código a seguir demonstra o uso do Increment.

// C#
private static ManualResetEvent[] manualResetEvents;

public void Teste()
{
    int intQuantidadeInteracoes = 10;
    int intContador = 0;

    manualResetEvents = new ManualResetEvent[intQuantidadeInteracoes];

    for (int i = 0; i < intQuantidadeInteracoes; i++)
    {
        manualResetEvents[intContador] = new ManualResetEvent(false);

        ThreadPool.QueueUserWorkItem(this.Executar, intContador);

        Interlocked.Increment(ref intContador);
    }

    WaitHandle.WaitAll(manualResetEvents);
}

public void Executar(object intContador)
{
    Console.WriteLine("Posição atual do contador: {0}",
        Convert.ToInt32(intContador));

    manualResetEvents[Convert.ToInt32(intContador)].Set();
}
' VB.NET
Private Shared manualResetEvents As ManualResetEvent()

Public Sub Teste()
    Dim intQuantidadeInteracoes As Integer = 10
    Dim intContador As Integer = 0

    manualResetEvents = New ManualResetEvent(intQuantidadeInteracoes - 1) {}

    For i As Integer = 0 To intQuantidadeInteracoes - 1
        manualResetEvents(intContador) = New ManualResetEvent(False)

        ThreadPool.QueueUserWorkItem(AddressOf Me.Executar, intContador)

        Interlocked.Increment(intContador)
    Next

    WaitHandle.WaitAll(manualResetEvents)
End Sub

Public Sub Executar(intContador As Object)
    Console.WriteLine("Posição atual do contador: {0}", Convert.ToInt32(intContador))

    manualResetEvents(Convert.ToInt32(intContador)).[Set]()
End Sub

Referências

Novo artigo no iMasters

Deixe um comentário

Mais um artigo publicado no iMasters:

Recebendo retornos de threads em C#

Existem situações em que é necessário retornar informações quando a execução da thread estiver concluída para que elas sejam usadas na continuação do processamento da thread principal da aplicação. O intuito deste artigo é mostrar uma maneira de receber e tratar retornos de threads.

Neste link: imasters.com.br/artigo/19615/c-sharp/recebendo-retornos-de-threads-em-c

Vale a pena conferir!

Obrigado.

Artigo no iMasters

Deixe um comentário

Tive meu primeiro artigo publicado no iMasters. Apesar de ser um artigo que já havia publicado aqui no blog, vale a pena conferir pois fiz pequenas modificações para melhorar o conteúdo.

Programando threads em C#

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.

Neste link: http://imasters.com.br/artigo/19541/csharp/programando_threads_em_c/

Obrigado.

Passar parâmetros para Threads

3 comentários

Há algum tempo, escrevi um post que mostra formas de programar threads em C# e VB.NET. Na ocasião, mostrei uma forma de passar parâmetros para uma thread. O intuito deste post é mostrar uma outra forma existente para este mesmo objetivo.

Primeiro, criamos uma classe que conterá propriedades que representarão os parâmetros que devem ser passados para a Thread.

// C#
public class DadosLog
{
    public Exception Exception { get; set; }
    public string StrNomeProjeto { get; set; }
    public string StrNomeClasse { get; set; }
    public string StrNomeMetodo { get; set; }
}
' VB.NET
Public Class DadosLog
    Public Property Exception() As Exception
        Get
            Return _Exception
        End Get
        Set(ByVal value As Exception)
            _Exception = value
        End Set
    End Property
    Private _Exception As Exception
    Public Property StrNomeProjeto() As String
        Get
            Return _StrNomeProjeto
        End Get
        Set(ByVal value As String)
            _StrNomeProjeto = value
        End Set
    End Property
    Private _StrNomeProjeto As String
    Public Property StrNomeClasse() As String
        Get
            Return _StrNomeClasse
        End Get
        Set(ByVal value As String)
            _StrNomeClasse = value
        End Set
    End Property
    Private _StrNomeClasse As String
    Public Property StrNomeMetodo() As String
        Get
            Return _StrNomeMetodo
        End Get
        Set(ByVal value As String)
            _StrNomeMetodo = value
        End Set
    End Property
    Private _StrNomeMetodo As String
End Class

Em seguida, o método que executará o processo em segundo plano deve ter como parâmetro de entrada um object.

// C#
public void ExecutarLogErro(object _dadosLog)
{
    DadosLog dadosLog = (_dadosLog as DadosLog);

    // Gravar Log no Repositório ou no Log de Eventos do Windows
}
' VB.NET
Public Sub ExecutarLogErro(ByVal _dadosLog As Object)
    Dim dadosLog As DadosLog = TryCast(_dadosLog, DadosLog)

    ' Gravar Log no Repositório ou no Log de Eventos do Windows
End Sub

Para concluir, passamos a instância da classe DadosLog no método Start() no momento da chamada à thread.

// C#
public void LogarErro(Exception exception,
    string strNomeProjeto,
    string strNomeClasse,
    string strNomeMetodo)
{
    DadosLog dadosLog = new DadosLog()
    {
        Exception = exception,
        StrNomeClasse = strNomeClasse,
        StrNomeMetodo = strNomeMetodo,
        StrNomeProjeto = strNomeProjeto
    };

    GerenciaLogErro gle = new GerenciaLogErro();
    Thread t = new Thread(gle.ExecutarLogErro);
    t.IsBackground = true;
    t.Start(dadosLog);
}
' VB.NET
Public Sub LogarErro(ByVal exception As Exception, _
                     ByVal strNomeProjeto As String, _
                     ByVal strNomeClasse As String, _
                     ByVal strNomeMetodo As String)
    Dim dadosLog As New DadosLog() With { _
     .Exception = exception, _
     .StrNomeClasse = strNomeClasse, _
     .StrNomeMetodo = strNomeMetodo, _
     .StrNomeProjeto = strNomeProjeto _
    }

    Dim gle As New GerenciaLogErro()
    Dim t As New Thread(AddressOf gle.ExecutarLogErro)
    t.IsBackground = True
    t.Start(dadosLog)
End Sub

Essa é apenas mais uma maneira de passar informações para a Thread. Existem outras e mais para frente tentarei demonstrar aqui.

OBS: Este código fonte pode ser baixado aqui.

Obrigado.

Código fonte do artigo “Programando Threads em C# e VB.NET”

Deixe um comentário

Há alguns dias publiquei um artigo intitulado Programando Threads em C# e VB.NET. Algumas pessoas me pediram o código fonte da solução que criei para demonstração. Consegui postar o código fonte apenas hoje.

Download através do 4Shared neste link. (OBS: Arquivo compactado no formato ZIP)

Tutorial de utilização do 4Shared neste link.

OBS: Há algum tempo o 4Shared está exigindo que o usuário esteja logado para fazer download de arquivo, por isso passei a postar os arquivos no DropBox. Caso não consiga fazer o download no 4Shared, utilize este link do DropBox para baixar o arquivo.

Requisitos: Visual Studio 2010 e .Net Framework 4.

Obrigado.

Programando Threads em C# e VB.NET

7 comentários

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