Durante nossas aulas de sistemas operacionais, chegamos a um dos tópicos mais fundamentais para entender como os computadores modernos executam múltiplas tarefas de forma eficiente: threads e concorrência. Nesta aula, exploramos o que são threads, como elas se diferenciam de processos tradicionais, os modelos de implementação e os desafios de sincronização que surgem quando múltiplas linhas de execução acessam recursos compartilhados.
O que são threads?
Uma thread, também chamada de linha de execução, é a menor unidade de processamento que pode ser escalonada pelo sistema operacional. Diferente de um processo, que possui seu próprio espaço de endereçamento, recursos e contexto, uma thread compartilha o mesmo espaço de endereçamento e recursos com outras threads do mesmo processo.
Pense em um processo como uma "casa" — ele tem seus próprios cômodos (espaço de endereçamento), móveis (recursos alocados) e portas (descritores de arquivo). As threads seriam as "pessoas" morando nessa casa: todas compartilham o mesmo espaço, mas cada uma pode estar fazendo uma atividade diferente ao mesmo tempo.
Na prática, threads de um mesmo processo podem:
- Compartilhar variáveis globais e dados alocados no heap
- Acessar os mesmos descritores de arquivo
- Comunicar-se diretamente através da memória compartilhada
- Ser escalonadas independentemente pelo sistema operacional
Diferença entre threads e processos
Uma das primeiras perguntas que surgiu durante a aula foi: por que usar threads em vez de múltiplos processos? A resposta está na eficiência. Criar um novo processo é uma operação custosa — o sistema operacional precisa alocar um novo espaço de endereçamento, inicializar estruturas de dados, copiar contextos e configurar tabelas de páginas. Threads, por outro lado, são muito mais leves.
| Característica | Processo | Thread |
|---|---|---|
| Espaço de endereçamento | Próprio e isolado | Compartilhado com o processo pai |
| Criação | Custosa (syscall pesada) | Leve (criação rápida) |
| Comunicação | IPC (pipes, filas, sockets) | Memória compartilhada diretamente |
| Chaveamento de contexto | Mais lento | Mais rápido |
| Isolamento | Total (um processo não afeta outro) | Parcial (uma thread pode corromper dados de outra) |
| Escalonamento | SO escala processos | SO escala threads (no modelo kernel) |
Discutimos como o chaveamento de contexto entre threads é significativamente mais rápido porque não envolve a troca do espaço de endereçamento — apenas registradores e pilha precisam ser salvos e restaurados.
Threads em userspace vs threads em kernel
Outro ponto importante foi entender os dois modelos principais de implementação de threads: threads em nível de usuário (user-level threads) e threads em nível de kernel (kernel-level threads).
Threads em userspace são gerenciadas inteiramente por uma biblioteca em espaço de usuário, sem envolvimento do kernel. O escalonamento é feito pela própria aplicação. Vantagens:
- Criação e chaveamento extremamente rápidos (não requerem syscalls)
- Podem ser implementadas mesmo em sistemas que não suportam threads nativamente
- Maior controle sobre a política de escalonamento
Desvantagens:
- Se uma thread faz uma chamada de sistema bloqueante, todo o processo bloqueia
- Não aproveitam múltiplos núcleos de CPU (apenas uma thread do processo roda por vez no kernel)
- O escalonamento pode ser injusto se a biblioteca não implementar preempção corretamente
Threads em kernel são gerenciadas pelo sistema operacional. Cada thread é uma entidade separada que o kernel pode escalonar individualmente. Vantagens:
- Aproveitam verdadeiramente múltiplos núcleos (paralelismo real)
- Threads bloqueantes não afetam outras threads do mesmo processo
- O escalonamento é feito pelo SO, que tem visão global do sistema
Desvantagens:
- Criação e chaveamento são mais lentos (requerem syscalls)
- Consomem mais recursos do kernel (cada thread precisa de estrutura no kernel)
- Maior complexidade no gerenciamento de estado
Sistemas modernos (Linux, Windows, macOS) implementam threads em nível de kernel, geralmente seguindo o modelo one-to-one. O Linux implementa threads através da chamada de sistema clone(), que permite criar novos processos compartilhando o espaço de endereçamento.
Sincronização: mutex e semáforos
Quando múltiplas threads acessam dados compartilhados simultaneamente, surge o problema da condição de corrida (race condition). Para evitar isso, precisamos de mecanismos de sincronização.
Mutex (Mutual Exclusion) — Uma variável de exclusão mútua que permite que apenas uma thread por vez acesse uma seção crítica. Se uma thread tenta adquirir um mutex já ocupado, ela é bloqueada até que o mutex seja liberado.
// Pseudocódigo de uso de mutex
mutex_lock(&meu_mutex);
// Seção crítica — apenas uma thread executa aqui
contador++;
mutex_unlock(&meu_mutex);
Semáforo — Um mecanismo mais geral que controla o acesso a um recurso com um número limitado de instâncias. Um semáforo possui um contador interno e duas operações: wait() (decrementa o contador, bloqueando se zero) e signal() (incrementa o contador, liberando threads bloqueadas). Semáforos podem ser binários (funcionam como mutex) ou contadores (para gerenciar múltiplas instâncias).
Problemas clássicos de concorrência
Para consolidar o entendimento, estudamos três problemas clássicos que ilustram os desafios da programação concorrente:
1. Produtor-Consumidor (Bounded Buffer) — Um ou mais produtores geram dados colocados em um buffer compartilhado, e um ou mais consumidores retiram esses dados. O desafio é coordenar o acesso ao buffer evitando que produtores insiram em buffer cheio e consumidores retirem de buffer vazio.
2. Leitores-Escritores (Readers-Writers) — Múltiplas threads podem ler dados compartilhados simultaneamente, mas apenas uma pode escrever por vez. O problema envolve garantir que nunca haja um escritor escrevendo enquanto leitores estão lendo, evitando starvation de ambos os lados.
3. Jantar dos Filósofos (Dining Philosophers) — Cinco filósofos sentam-se em uma mesa redonda, cada um com um prato de espaguete. Entre cada par há um garfo. Cada filósofo alterna entre pensar e comer, mas precisa de dois garfos para comer. Ilustra deadlock, starvation e como projetar algoritmos de sincronização robustos.
Pontos importantes desta aula
- Threads permitem paralelismo real em sistemas multiprocessados e concorrência eficiente em monoprocessados
- O modelo de threads em kernel (one-to-one) é o padrão em sistemas modernos
- Sincronização é essencial para evitar condições de corrida, mas deve ser usada com cuidado para evitar deadlocks
- Mutex e semáforos são mecanismos complementares com aplicações distintas
- Os problemas clássicos de concorrência aparecem com frequência em sistemas reais
Perguntas frequentes
Qual a diferença entre concorrência e paralelismo?
Concorrência é a capacidade de lidar com múltiplas tarefas ao mesmo tempo, mas não necessariamente executando-as simultaneamente (pode haver chaveamento). Paralelismo é a execução simultânea real em múltiplos núcleos de CPU.
Uma thread pode acessar variáveis locais de outra thread?
Não diretamente. Cada thread tem sua própria pilha; variáveis locais são alocadas na pilha. O compartilhamento ocorre através de variáveis globais, dados no heap ou memória compartilhada explicitamente.
O que é uma função thread-safe?
É uma função que pode ser chamada por múltiplas threads simultaneamente sem causar condições de corrida. Geralmente evita variáveis estáticas ou globais, ou utiliza mecanismos de sincronização.
Como evitar deadlocks?
Seguindo boas práticas: adquirir locks sempre na mesma ordem, usar timeouts, minimizar o tempo de retenção de locks e, quando possível, usar algoritmos lock-free.