🔀
Threads vs processos vs fibras: modelo de concorrência
⏱ 15 min de leitura·+75 XP
Concorrência pode ser implementada de formas radicalmente diferentes: threads do SO, processos, goroutines e event loops. Cada modelo tem trade-offs de custo de criação, overhead de comunicação e complexidade de sincronização.
Processo vs Thread: o que o kernel vê
| Aspecto | Processo | Thread (mesm. processo) |
|---|---|---|
| Memória | Espaço virtual próprio | Compartilhada com todos do processo |
| File descriptors | Próprios (herdados no fork) | Compartilhados |
| Criação (Linux) | fork() ~1ms | pthread_create() ~0.1ms |
| Context switch | 3-10µs (TLB flush) | 1-2µs (mesma tabela de páginas) |
| Comunicação | IPC (pipe, socket, shm) | Memória compartilhada direta |
| Isolamento | Total — crash não afeta outros | Partial — crash mata todos os threads |
| GIL Python | Não compartilhado (cada processo tem o seu) | Compartilhado (um thread por vez) |
Green threads e M:N threading
# Comparação de custo de criação e context switch:
import threading, time, asyncio
# Thread do SO — ~0.1ms para criar, ~1-2µs para context switch
def tarefa_thread():
time.sleep(0.001) # bloqueia thread
inicio = time.time()
threads = [threading.Thread(target=tarefa_thread) for _ in range(1000)]
for t in threads: t.start()
for t in threads: t.join()
print(f"1000 threads: {time.time()-inicio:.2f}s") # ~1-2s de overhead
# Coroutine asyncio — ~0µs para criar, zero context switch de kernel
async def tarefa_async():
await asyncio.sleep(0.001) # cede controle, não bloqueia
async def main():
inicio = time.time()
await asyncio.gather(*[tarefa_async() for _ in range(10_000)])
print(f"10000 coroutines: {time.time()-inicio:.2f}s") # ~0.01s!
asyncio.run(main())
# Go goroutines (conceito — Python não tem equivalente nativo):
# go minha_funcao() # cria goroutine com 2KB de stack
# São M:N: N goroutines mapeadas para M threads do OS
# runtime.GOMAXPROCS(4) = 4 threads do OS para N goroutines
# Go scheduler: work-stealing, preemptivo, cooperativo em I/O
# Erlang/Elixir processes (similar a goroutines mas com isolamento de memória):
# spawn(fn -> ... end) # processo Erlang leve, ~300 bytes
# Comunicação por message passing — sem memória compartilhada
# Crash de um processo não afeta outros (fault isolation)Escolhendo o modelo de concorrência
# DECISION TREE:
#
# Preciso de paralelismo CPU-bound?
# └── Sim → multiprocessing.Process ou ProcessPoolExecutor
# (cada processo tem seu GIL em Python)
#
# Preciso de I/O concorrente (muitas conexões/requests)?
# ├── Código existente síncrono → ThreadPoolExecutor
# └── Código novo → asyncio + httpx/aiohttp/asyncpg
#
# Muitas conexões leves (>1000)?
# └── asyncio (event loop sobre epoll)
#
# Comunicação entre workers sem overhead de pickle/IPC?
# └── threads (memória compartilhada) — cuidado com race conditions
#
# Isolamento de falhas crítico?
# └── processos separados (crash de um não afeta outros)
# Python multiprocessing (processos reais):
from multiprocessing import Process
def worker(n):
print(f"Worker {n}, PID: {os.getpid()}")
# Memória isolada — crash aqui não afeta main
processos = [Process(target=worker, args=(i,)) for i in range(4)]
for p in processos: p.start()
for p in processos: p.join()
# Verificar threads de um processo (em Linux):
# ps -eLf | grep python
# cat /proc/$(pgrep python)/status | grep Threads✅
Resumo prático: threads para I/O concorrente com código síncrono. asyncio para I/O concorrente com código novo. multiprocessing para paralelismo CPU real. Goroutines/Erlang processes para sistemas com muita concorrência leve e isolamento de falhas. O GIL Python torna threads inadequadas para CPU-bound — use multiprocessing.
💡
Próximo: Containers por baixo — namespaces e cgroups do Linux que o Docker usa.
🧩
Quiz rápido
3 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito
Continue lendo