Concorrência vs Paralelismo em Python: O Que Realmente Importa
Concorrência e paralelismo são frequentemente confundidos, mas resolvem problemas diferentes. Entender essa distinção ajuda você a escolher o modelo correto em Python: async, threads ou processos.
Muitos problemas de desempenho em Python não são causados por código lento. Eles são causados pela escolha do modelo de execução errado.
Concorrência e paralelismo são conceitos relacionados, mas não são a mesma coisa. Quando você entende a diferença, consegue tomar decisões melhores sobre asyncio, threads, processos e arquitetura de sistemas.
A Ideia Central
Concorrência trata de lidar com múltiplas tarefas fazendo progresso em mais de uma ao mesmo tempo.
Paralelismo trata de executar múltiplas tarefas simultaneamente.
Um sistema concorrente pode alternar entre tarefas e continuar responsivo mesmo que elas não estejam sendo executadas exatamente ao mesmo tempo. Um sistema paralelo aumenta o throughput executando tarefas simultaneamente, normalmente utilizando múltiplos núcleos de CPU.
Um Modelo Mental Simples
Imagine um serviço web recebendo várias requisições.
- Concorrência permite lidar com muitas requisições sem bloquear durante operações lentas de I/O, como chamadas de rede ou consultas ao banco de dados.
- Paralelismo permite acelerar trabalho pesado de CPU, como processamento de imagens ou cálculos intensivos.
Um erro comum é tentar usar ferramentas de concorrência para acelerar tarefas CPU-bound, ou usar paralelismo quando o gargalo real está em operações de I/O.
Concorrência em Python
Concorrência em Python é frequentemente usada para melhorar a responsividade e aproveitar melhor o tempo de espera das operações.
Abordagens comuns incluem:
asynciopara agendamento cooperativo de tarefas I/O-boundthreadingpara tarefas I/O-bound que utilizam bibliotecas bloqueantes- loops de eventos e I/O não bloqueante
Concorrência com asyncio
import asyncio
async def fetch_data(i: int) -> str:
await asyncio.sleep(1)
return f"dado-{i}"
async def main():
tasks = [fetch_data(i) for i in range(3)]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
Aqui temos concorrência. As tarefas fazem progresso enquanto outras estão aguardando.
Esse modelo funciona muito bem para workloads I/O-bound, como chamadas HTTP, consultas a banco de dados, filas de mensagens ou operações com arquivos.
Threads como ferramenta prática de concorrência
from threading import Thread
import time
def io_task(i: int) -> None:
time.sleep(1)
print(f"concluido-{i}")
threads = [Thread(target=io_task, args=(i,)) for i in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
Mesmo no CPython, threads podem ajudar quando o workload passa tempo esperando por I/O.
Paralelismo em Python
Paralelismo busca executar mais trabalho de CPU em menos tempo real, executando tarefas simultaneamente.
No CPython, paralelismo real para tarefas CPU-bound normalmente utiliza múltiplos processos, não threads.
Paralelismo com multiprocessing
from multiprocessing import Pool
def compute(x: int) -> int:
return x * x
if __name__ == "__main__":
with Pool(4) as p:
result = p.map(compute, range(10))
print(result)
Cada processo possui seu próprio interpretador Python e seu próprio GIL, permitindo utilizar múltiplos núcleos de CPU ao mesmo tempo.
Essa abordagem é adequada para workloads pesados de CPU, como processamento de dados, transformações em lote, parsing ou cálculos numéricos.
O GIL e Por Que Ele Importa
No CPython existe o Global Interpreter Lock, conhecido como GIL.
Ele permite que apenas uma thread execute bytecode Python por vez dentro de um processo.
Isso traz algumas consequências importantes:
- Threads não aceleram código Python CPU-bound na maioria dos casos.
- Threads continuam sendo eficazes para workloads I/O-bound.
- Processos permitem paralelismo real para tarefas CPU-bound.
Uma regra simples ajuda bastante:
Se a tarefa está esperando, use concorrência.
Se a tarefa está calculando, use paralelismo.
Um Exemplo Prático de Sistemas Reais
Em sistemas reais, utilizei os dois modelos dependendo do gargalo.
Ao integrar múltiplas APIs externas, onde a latência de rede dominava o tempo de execução, usar concorrência com asyncio aumentou significativamente o throughput sem elevar o uso de CPU.
Já em pipelines de processamento de dados, onde parsing e transformações pesadas eram CPU-bound, migrar para multiprocessing reduziu o tempo total de execução ao distribuir o trabalho entre vários núcleos.
A principal lição não foi escolher uma ferramenta primeiro, mas identificar o que realmente estava lento.
Escolhendo o Modelo Certo
Antes de mudar a arquitetura do sistema, vale perguntar:
- O workload está principalmente esperando ou calculando?
- O gargalo está na rede, no disco, no banco de dados ou na CPU?
- As bibliotecas utilizadas são compatíveis com async ou bloqueantes?
- O workload é grande o suficiente para justificar o overhead de processos?
Use concorrência quando:
- O workload é I/O-bound
- Você precisa de alta responsividade
- Quer evitar bloquear o fluxo principal
Use paralelismo quando:
- O workload é CPU-bound
- Você quer utilizar múltiplos núcleos de CPU
- Está executando cálculos pesados ou jobs em lote
Conclusão
Concorrência e paralelismo são ferramentas complementares.
Concorrência ajuda o sistema a permanecer eficiente enquanto espera.
Paralelismo permite executar mais trabalho de CPU utilizando múltiplos núcleos.
A verdadeira habilidade não está em conhecer as APIs, mas em saber qual modelo corresponde ao gargalo do seu sistema.