1. Introdução
Multithreading é como o trabalho paralelo de vários colegas no escritório: um imprime documentos, outro liga para o cliente, o terceiro faz café (e, claro, todos são programadores). Se tivéssemos apenas um colega, ele faria tudo em sequência, e o escritório afogaria em tédio e fila pelo café. Em programação a situação é análoga: um programa single-thread só pode executar uma tarefa por vez.
Imagine que nossa aplicação executa uma operação longa, por exemplo, baixa um arquivo da internet ou calcula uma enorme tabela. Todo o resto nesse momento "congela" — botões não respondem, animação não anda, a mensagem "não responde" aparece em uma janela popup.
Multithreading permite que a aplicação faça várias coisas ao mesmo tempo: a interface permanece responsiva, operações rodam em paralelo, e não temos que entrar em um monólogo estilo "Computador, você travou de novo?!".
Conceitos e terminologia básicos
Antes de mergulhar de cabeça, vamos entender o que é um thread e em que ele difere de um processo.
- Processo (Process): Programa independente com seu próprio espaço de endereço, variáveis e recursos. Por exemplo, cada aplicativo aberto no Windows é um processo separado.
- Thread (Thread): Unidade de execução dentro de um processo. Um processo pode conter uma ou várias threads, que compartilham os mesmos recursos (memória, variáveis).
Por que multithreading gera tantas dúvidas?
Porque threads são caras instáveis: elas podem começar a executar a qualquer momento, misturar dados, interromper umas às outras e basicamente fazer bagunça na memória se você não controlar a ordem. Se parece um berçário sem monitor — é isso mesmo! Disciplina e cuidado com threads são a base para escrever programas multithread confiáveis.
2. História e papel do multithreading em C# e .NET
Nos velhos tempos C# era mais single-threaded e os programas eram simples. Com o aumento das exigências de performance, o surgimento de CPUs multicore e a necessidade de criar apps responsivos sem travamentos de UI, o .NET ganhou ferramentas para multithreading. Primeiro veio o clássico System.Threading.Thread, depois apareceram Tasks (Task), métodos assíncronos (async/await), processamento paralelo de dados (PLINQ) e primitivos de sincronização de alto nível.
C# evoluiu para uma plataforma poderosa onde multithreading não é exótico, é cotidiano.
Visualmente: processo e threads
Aqui vai um esquema simples:
+--------------------------------------------------+
| Processo (seu programa) |
| +-------------+ +-------------+ |
| | Thread 1 | | Thread 2 | |
| +-------------+ +-------------+ |
| ... |
| +-------------+ |
| | Thread N | |
| +-------------+ |
+--------------------------------------------------+
Todos os threads dentro do processo veem variáveis e recursos compartilhados.
3. Como criar um thread em C#?
Vamos começar pelo mais básico: a classe Thread do namespace System.Threading.
Exemplo: iniciando o segundo thread
Suponha que temos uma tarefa longa — por exemplo, calcular a soma dos números de 1 até 10_000_000. Enquanto o cálculo ocorre, o thread principal escreve uma saudação para o usuário.
using System;
using System.Threading;
class Program
{
// Método para a segunda tarefa
static void CalculateSum()
{
long sum = 0;
for (int i = 1; i <= 10_000_000; i++)
sum += i;
Console.WriteLine($"[Thread 2] Soma: {sum}");
}
static void Main()
{
// Criamos o thread, informamos o delegate pro método
Thread thread = new Thread(CalculateSum);
thread.Start(); // Inicia o segundo thread
// O thread principal continua trabalhando
Console.WriteLine("[Thread 1] Olá! Trabalhando em paralelo...");
// Esperamos o término do segundo thread antes de sair
thread.Join();
Console.WriteLine("[Thread 1] Tudo concluído!");
}
}
O que vai acontecer?
Na tela aparecerá a linha "[Thread 1] Olá! Trabalhando em paralelo...", e depois, quando o thread de cálculo terminar, ele vai imprimir a soma.
Problema típico: quem chega primeiro imprime
Tente executar esse código várias vezes — a ordem das linhas pode variar! Às vezes a soma aparece primeiro, às vezes a saudação. Isso é multithreading real — programas ficam menos previsíveis, como o humor de um gato às segundas-feiras.
4. Zona de memória: o que os threads veem?
Todos os threads dentro de um processo têm acesso às mesmas variáveis (exceto variáveis locais de método). Se uma variável é modificada em um thread — as outras threads vão ver essa mudança!
Exemplo: variável compartilhada
using System;
using System.Threading;
class Program
{
static int counter = 0;
static void Increment()
{
for (int i = 0; i < 1000; i++)
counter++;
}
static void Main()
{
Thread t1 = new Thread(Increment);
Thread t2 = new Thread(Increment);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine($"counter = {counter}");
}
}
Quanto esperamos ver em counter? A lógica sugere 2000, já que cada thread incrementa 1000 vezes.
Mas não! Execute várias vezes — você verá valores diferentes: 1782, 1935, 1999…
Por quê? Esse é o problema clássico de Race Condition — os threads "se atropelam" entre leitura -> incremento -> escrita, e alguns incrementos se perdem.
5. Dicas úteis
Como threads interagem com a interface?
Em aplicações desktop modernas (WinForms/WPF/MAUI) o thread principal atende a interface gráfica do usuário. Todas as ações do usuário (cliques, input) ocorrem nesse thread. Tarefas de fundo devem rodar em outros threads, mas você não pode, por regra, mexer diretamente na UI a partir de threads diferentes. Isso evita caos.
No console não há essa restrição, você pode escrever com Console.WriteLine de qualquer thread. Contudo, em apps reais sem sincronização correta a UI pode "desandar".
Sequencialidade vs paralelismo
Tabela para fixar as diferenças.
| Código single-thread | Código multithread |
|---|---|
| Executa tarefas em sequência | Tarefas podem ser executadas simultaneamente |
| UI "trava" em tarefas longas | UI permanece responsiva |
| Fácil ler e escrever variáveis | Requer controle de acesso aos dados |
| Fácil depurar | Pode ser difícil depurar |
Pontos importantes ao trabalhar com threads
- Variáveis compartilhadas — risco compartilhado. Como mostrado acima, se vários threads usam uma variável compartilhada — sem sincronização podem ocorrer erros! (Mais detalhes em aulas futuras.)
- Você pode "esperar" um thread com Join(). O método Join() permite pausar o thread principal até o término do background thread. Use-o quando precisar aguardar um resultado.
- Um thread não pode ser iniciado duas vezes. Depois que um thread termina, não dá pra Start() de novo — é preciso criar um novo objeto Thread.
- Término do thread. Um thread termina quando o método que ele executa chega ao fim. Matar threads à força é uma má ideia (o método Abort() é considerado nocivo e obsoleto).
Para que serve multithreading na prática?
- Apps com UI: evitar congelamento da interface enquanto ocorrem downloads ou cálculos em background.
- Servidores e serviços: processar requisições de muitos clientes simultaneamente.
- Computação de alta performance: dividir uma grande tarefa (por exemplo, processar milhões de registros) em partes e executar em paralelo.
- Games, simulações, processamento de dados: modelar sistemas complexos sem perder performance.
Problemas do multithreading
Multithreading dá poder, mas também adiciona complexidade:
- Race Condition (condição de corrida): quando vários threads modificam os mesmos dados ao mesmo tempo, o resultado depende da ordem das instruções, que é imprevisível.
- Deadlock (bloqueio mútuo): threads ficam esperando uns aos outros e ninguém consegue continuar.
- Starvation (inanição): um dos threads fica constantemente sem acesso ao recurso.
Nesta aula só mencionamos os problemas principais do multithreading; nas próximas vamos aprender a reconhecê-los e evitá-los. Por enquanto — lembre-se: se algo "travou", a culpa pode ser não só de bugs, mas também de threads que decidiram "fazer uma festa".
GO TO FULL VERSION