1. Introdução
Se você imaginar um processo como um supermercado, as threads (thread) são os caixas em diferentes balcões, atendendo clientes ao mesmo tempo. Todos os caixas trabalham na mesma loja, mas cada um executa sua tarefa em paralelo, fazendo o trabalho ficar mais rápido e eficiente. Threads dentro de um processo permitem executar várias tarefas simultaneamente, compartilhando recursos comuns e coordenando o trabalho dentro de uma única aplicação. A principal vantagem é que as threads realmente podem rodar em paralelo, se o processador suportar multitarefa.
Necessidade prática de threads
Por que a gente quer rodar várias threads? Eis algumas situações do mundo real:
- Você está escrevendo uma aplicação com interface gráfica e não quer que ela "congele" durante uma operação longa.
- Você precisa baixar vários arquivos ao mesmo tempo.
- Em um jogo, inimigos devem "pensar" independentemente uns dos outros.
É surpreendente, mas em muitos programas ainda aparecem "congelamentos" por causa do uso indevido de threads. Hoje vamos aprender a evitar esses perrengues.
Classe Thread: a base do multithreading manual
A classe Thread é um dinossauro no mundo do multithreading no .NET. Apesar do surgimento de ferramentas mais modernas (Task, async/await), trabalhar com Thread ainda faz sentido, especialmente se você quer sentir que está no controle das threads "do zero".
Esquema para criar uma thread
- Criar um objeto da classe Thread, passando o método que será executado na thread.
- Iniciar a thread usando o método Start().
- (Opcional) Observar o que acontece — afinal, tudo pode rodar em paralelo!
2. Iniciando uma thread com Thread
Vamos sentir na prática a "mágica" do paralelismo. Vamos adicionar na nossa aplicação hipotética uma classe simples que conta até um número e imprime o progresso. Vamos aprender a rodar esse trabalho em uma thread separada.
using System;
using System.Threading;
class Program
{
static void Main()
{
Console.WriteLine("Thread principal iniciada!");
// Criamos o objeto thread, apontando para o método a ser executado
Thread workerThread = new Thread(CountToTen);
// Iniciamos a nova thread
workerThread.Start();
// A thread principal também faz algo: escreve pontos...
for (int i = 0; i < 5; i++)
{
Console.Write(".");
Thread.Sleep(500); // Pausa para ficar visível
}
Console.WriteLine("\nThread principal finalizada!");
}
static void CountToTen()
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"[Thread] Contando: {i}");
Thread.Sleep(400);
}
Console.WriteLine("[Thread] Pronto!");
}
}
O que acontece?
Você vai ver no console que os pontos e "Contando: X" aparecem misturados. Esse é o primeiro sinal de multithreading! A thread principal escreve seus pontos, e a nova thread conta até 10. Elas não se atrapalham, como dois músicos na banda: um toca bateria, o outro toca piano. Cada um tem seu som e, juntos, fazem música.
3. Como passar dados para a thread?
Às vezes a thread precisa saber não só o que fazer, mas com o que trabalhar. Se o método da thread aceita parâmetros, como passá-los?
Opção 1: Usando uma lambda (método anônimo)
int bounds = 7;
Thread t = new Thread(() => CountToNumber(bounds));
t.Start();
static void CountToNumber(int n)
{
for (int i = 1; i <= n; i++)
{
Console.WriteLine($"[Thread] {i} / {n}");
Thread.Sleep(300);
}
}
Aqui embrulhamos a chamada do método desejado numa lambda para passar parâmetros. Isso é bem comum, porque Thread espera um método sem parâmetros (ThreadStart).
Opção 2: Usando ParameterizedThreadStart
Você pode usar o delegado especial ParameterizedThreadStart, que aceita um parâmetro do tipo object.
Thread t = new Thread(CountToNumberObject);
t.Start(12);
static void CountToNumberObject(object? n)
{
int max = (int)n!;
for (int i = 1; i <= max; i++)
{
Console.WriteLine($"[Thread] {i} / {max}");
Thread.Sleep(200);
}
}
Sim, o tipo do parâmetro é object, então é necessário fazer cast. Não é o ideal, mas funciona! No C# moderno a lambda costuma ser a opção preferida.
4. Gerenciando o ciclo de vida da thread
Vamos ver as ferramentas úteis que a classe Thread oferece.
| Propriedade / Método | Propósito |
|---|---|
|
Inicia a thread (o método indicado na criação) |
|
Aguarda a conclusão da thread (bloqueia a thread chamadora até a outra terminar) |
|
Mostra se a thread está rodando agora (true/false) |
|
Permite dar um nome à thread (útil pra debugging) |
|
Obter o objeto que representa a thread atual |
|
Pausa a thread atual por ms milissegundos |
Exemplo: Esperando a thread terminar
Às vezes é necessário que a thread principal espere a finalização de uma thread auxiliar.
Thread t = new Thread(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"[Segunda thread] {i}");
Thread.Sleep(300);
}
});
t.Start();
Console.WriteLine("[Thread principal] Aguardando a finalização da segunda thread...");
t.Join(); // A thread principal espera aqui
Console.WriteLine("[Thread principal] A segunda thread terminou!");
Sem Join() o programa poderia terminar mesmo com a thread ainda rodando. Com Join() a thread principal espera pacientemente até tudo acabar.
5. Dicas úteis
Nomeando threads: para não se perder
Para facilitar o debugging você pode dar nomes às threads:
Thread t = new Thread(() =>
{
Console.WriteLine($"Isso está rodando na thread: {Thread.CurrentThread.Name}");
});
t.Name = "Thread-Contador";
t.Start();
Isso ajuda quando há muitas threads e cada uma faz sua parte.
Limitações e o futuro real
Vale dizer desde já: em apps modernos o gerenciamento manual de threads via Thread é raro. Na prática usa-se ferramentas mais poderosas e inteligentes (Task, async/await), que veremos em detalhe mais adiante. Mas entender os fundamentos de threads é importante para:
- Entender os bastidores do C# e do .NET.
- Entrevistas (às vezes vão pedir pra você explicar a diferença entre Thread e Task).
- Diagnosticar e resolver problemas complexos em aplicações grandes e legadas.
Esquema final: ciclo de vida da thread
stateDiagram-v2
[*] --> New: Thread criada
New --> Running: Start()
Running --> Stopped: Método finalizado
Stopped --> [*]
Agora você sabe criar e iniciar threads em C# por conta própria. Você já não é apenas um passageiro no trem, mas o maquinista que controla vários vagões! Na sequência vem o ciclo de vida da thread, sincronização e novos horizontes de paralelismo.
6. Erros comuns e truques ao trabalhar com Thread
Erro nº1: não iniciar a thread.
Muitas vezes criam-se objetos Thread mas esquecem de chamar Start(). Como resultado a thread não começa a rodar, e a razão pode não ser óbvia.
Erro nº2: alterar dados compartilhados sem sincronização.
Se várias threads mexem na mesma variável sem proteção, espere problemas! É como dois caixas tirando o troco da mesma caixinha — rapidamente vira bagunça e erros.
Erro nº3: usar métodos obsoletos e inseguros.
Não use Thread.Suspend(), Thread.Resume() e similares — eles são considerados perigosos e obsoletos. Gerencie o ciclo de vida das threads por outros meios.
Erro nº4: exceções não tratadas dentro de threads.
Se uma exceção ocorre dentro da thread e não é capturada, a thread termina e a thread principal pode nem perceber! Envolva o código da thread em um bloco try-catch para capturar e logar erros.
Thread t = new Thread(() =>
{
try
{
// ... seu código
}
catch (Exception ex)
{
// Logamos o erro
Console.WriteLine($"[Thread] Erro: {ex.Message}");
}
});
t.Start();
GO TO FULL VERSION