1. Introdução
Nas aulas anteriores, discutimos processos, comunicação entre processos e os primeiros conceitos de gerenciamento de memória. Hoje vamos mergulhar em um dos tópicos mais importantes para a construção de sistemas modernos e eficientes: threads. Uma thread (ou linha de execução) é a unidade básica de utilização da CPU, composta por um contador de programa, uma pilha e um conjunto de registradores. Enquanto um processo possui um espaço de endereçamento completo e isolado, múltiplas threads dentro de um mesmo processo compartilham esse espaço, incluindo o código, os dados abertos e os arquivos.
O conceito de threads surgiu da necessidade de executar múltiplas tarefas concorrentemente dentro de um mesmo ambiente, sem o alto custo de criação e gerenciamento de processos tradicionais. Um processo pesado (heavyweight process) carrega consigo um Context Block (PCB) extenso, enquanto uma thread é um "processo leve" (lightweight process). A criação de uma thread pode ser até 10x mais rápida que a criação de um processo em sistemas Linux modernos, e a comunicação entre threads é trivial, pois elas acessam diretamente as variáveis globais do processo pai.
2. Processos vs Threads
A principal diferença entre um processo e uma thread está no contexto de execução. Processos são isolados entre si por questões de segurança e estabilidade. Se um processo falha, os demais não são afetados. No entanto, essa proteção tem um custo: a criação de um processo exige a alocação de um novo PCB, uma nova tabela de páginas e um novo espaço de endereçamento. A comunicação entre processos (IPC) depende de mecanismos como pipes, filas de mensagens, sinais ou memória compartilhada, que adicionam overhead.
Threads, por outro lado, compartilham o mesmo espaço de endereçamento e os mesmos recursos do processo. Isso significa que:
- Compartilhamento de dados: threads podem ler e escrever nas mesmas variáveis globais sem a necessidade de mecanismos complexos de IPC.
- Economia de recursos: criar e destruir threads é muito mais rápido que criar e destruir processos.
- Comunicação direta: a troca de informações entre threads é feita simplesmente pela leitura e escrita na memória compartilhada.
- Escalabilidade: em sistemas multiprocessados (SMP), threads de um mesmo processo podem ser executadas em paralelo em diferentes núcleos da CPU, aumentando significativamente o throughput.
Uma analogia: um processo é como uma casa (com seu próprio endereço, cômodos e recursos). Threads são as pessoas que moram na casa e compartilham os mesmos cômodos, utensílios e comida. Elas podem conversar diretamente, mas precisam de cuidado para não usar o mesmo recurso ao mesmo tempo (exclusão mútua).
3. Vantagens do Uso de Multithreading
As aplicações modernas se beneficiam imensamente do uso de threads. As principais vantagens são:
- Responsividade: em aplicações interativas, uma thread pode ficar bloqueada aguardando uma operação de I/O (leitura de disco, requisição de rede), enquanto outra thread continua executando a interface com o usuário, mantendo o sistema responsivo. Navegadores web e editores de texto são exemplos clássicos.
- Compartilhamento de Recursos: como mencionado, threads compartilham naturalmente o espaço de endereçamento e os recursos do processo. Diferente de processos que precisam de mecanismos complexos.
- Economia: a criação e o chaveamento (context switch) de threads é mais barato. O chaveamento entre threads do mesmo processo não envolve a troca do espaço de endereçamento (apenas o registrador CR3 no x86-64 precisa ser mantido, o que é caro de trocar).
- Utilização de Multiprocessadores: em arquiteturas SMP ou NUMA, threads podem ser escalonadas para rodar simultaneamente em núcleos diferentes, alcançando paralelismo real e acelerando computações pesadas.
4. Modelos de Implementação de Threads
Existem duas categorias principais de threads, dependendo de onde são gerenciadas:
4.1 Threads em Nível de Usuário (ULT - User-Level Threads)
Gerenciadas por uma biblioteca no espaço do usuário (como GNU Pth ou fibras em Python). O kernel do sistema operacional não tem conhecimento da existência dessas threads. O chaveamento entre elas é feito inteiramente em espaço de usuário, sem chamadas de sistema, o que o torna extremamente rápido. O grande problema desse modelo é que, se uma thread faz uma chamada de sistema bloqueante (como read() em um arquivo ou sleep()), o processo inteiro é bloqueado, impedindo a execução das demais threads. Este é o modelo N:1 (várias threads de usuário mapeadas em um único thread de kernel).
4.2 Threads em Nível de Kernel (KLT - Kernel-Level Threads)
Gerenciadas diretamente pelo sistema operacional. O kernel é responsável pela criação, escalonamento e gerenciamento. Cada thread de usuário corresponde a uma thread de kernel (modelo 1:1). Sistemas como Windows, Linux (com NPTL - Native POSIX Thread Library) e macOS utilizam esse modelo. O chaveamento entre threads exige uma chamada de sistema (syscall), que é mais lenta que o chaveamento em nível de usuário. No entanto, o bloqueio de uma thread (por I/O, por exemplo) não bloqueia as outras threads do mesmo processo, permitindo verdadeiro paralelismo e concorrência eficiente.
4.3 Modelo M:N (Many-to-Many)
Uma abordagem híbrida, onde múltiplas threads de usuário (M) são mapeadas em um número menor ou igual de threads de kernel (N). O objetivo é combinar a rapidez do chaveamento em nível de usuário com o paralelismo oferecido pelo kernel. O kernel cria um número limitado de threads (um pool), e a biblioteca de usuário decide em qual delas executar. Esse modelo é complexo de implementar e, embora tenha sido usado no solaris e em versões antigas do linux, caiu em desuso com a eficiência do modelo 1:1 moderno.
5. Exemplo Prático (Python - threading)
Vamos ver um exemplo simples de concorrência com threads em Python. A função threading.Thread cria uma nova thread que executará a função alvo (target).
import threading
import time
def tarefa(nome, delay):
print(f"Iniciando thread {nome}")
time.sleep(delay)
print(f"Finalizando thread {nome}")
t1 = threading.Thread(target=tarefa, args=("A", 2))
t2 = threading.Thread(target=tarefa, args=("B", 1))
t1.start()
t2.start()
t1.join()
t2.join()
print("Ambas as threads finalizaram.")
Note que o escalonamento das threads não é determinístico. A saída pode variar entre execuções, e é por isso que, ao acessar recursos compartilhados, precisamos de mecanismos de sincronização.
6. Sincronização e Problemas de Concorrência
Quando múltiplas threads acessam e modificam uma variável compartilhada simultaneamente, pode ocorrer uma condição de corrida (race condition). O resultado final da computação depende da ordem de execução das threads. O exemplo clássico é a operação de incremento de uma variável:
# Exemplo de race condition
contador = 0
def incrementar():
global contador
for _ in range(1000000):
# O que parece ser uma linha é na verdade:
# 1. Ler contador (LOAD)
# 2. Incrementar (ADD)
# 3. Escrever contador (STORE)
contador += 1
# Criar duas threads
t1 = threading.Thread(target=incrementar)
t2 = threading.Thread(target=incrementar)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"Valor esperado: 2000000, Valor obtido: {contador}")
O resultado quase nunca será 2.000.000, pois as threads podem intercalar suas operações de leitura e escrita, resultando em perda de atualizações. Para evitar isso, usamos mecanismos de exclusão mútua, como Mutexes e Semáforos.
Um Mutex (Mutual Exclusion) garante que apenas uma thread execute uma seção crítica (trecho de código que acessa o recurso compartilhado) por vez. Em Python, usamos threading.Lock:
lock = threading.Lock()
contador = 0
def incrementar_seguro():
global contador
for _ in range(1000000):
with lock: # Adquire o lock
contador += 1 # Seção crítica
# Lock liberado automaticamente
O uso correto de locks garante a consistência dos dados, mas introduz o risco de deadlocks (impasse) se não for bem projetado, além de reduzir o paralelismo (serialização da seção crítica).
7. Conclusão
Threads são uma ferramenta essencial para escrever programas concorrentes e paralelos. Elas permitem melhor utilização dos recursos do sistema, maior responsividade em aplicações interativas e escalabilidade em ambientes multiprocessados. No entanto, trazem consigo a complexidade inerente à sincronização e ao gerenciamento de concorrência. A compreensão dos modelos de threads (ULT vs KLT) e dos problemas clássicos (race condition, deadlock, starvation) é fundamental para qualquer profissional da computação, seja trabalhando em sistemas operacionais, bancos de dados, servidores web ou aplicações de alta performance. Nas próximas aulas, exploraremos mais a fundo os algoritmos de escalonamento de threads e as técnicas avançadas de sincronização.
8. FAQ - Perguntas Frequentes
- Qual a diferença entre concorrência e paralelismo? Concorrência é a capacidade de lidar com múltiplas tarefas ao mesmo tempo, progredindo em períodos de tempo sobrepostos (pode ser em um único núcleo, com chaveamento). Paralelismo é a execução literalmente simultânea de tarefas em múltiplos núcleos de CPU.
- O que é uma thread daemon? Em Python, threads daemon são threads que são encerradas abruptamente quando o programa principal termina, sem esperar sua finalização. Úteis para tarefas de background.
- Threads em Python são realmente paralelas? Devido ao GIL (Global Interpreter Lock), threads em CPython não executam bytecode Python em paralelo. Para obter paralelismo real em Python, utiliza-se o módulo
multiprocessing, que cria processos separados. Threads são excelentes para tarefas de I/O-bound (redes, arquivos). - O que é um thread pool? Um conjunto de threads pré-criadas que ficam aguardando tarefas. Evita o overhead de criar e destruir threads constantemente. É amplamente usado em servidores web e frameworks concorrentes.
- Como faço para aprender mais sobre sistemas operacionais? Confira nossos outros posts na categoria Sistemas Operacionais ou navegue pela lista completa de artigos em Posts.