Curso de Python / Comparar threads, processos e asyncio com erros e cancelamento
Threads, processos e asyncio permitem avançar em várias tarefas com modelos diferentes. Escolha conforme o tempo gasto em cálculo ou espera e o suporte das bibliotecas; confirme o ganho com uma carga real antes de aumentar a concorrência.
Comparar concorrência e execução paralela
Concorrência significa organizar o progresso de mais de uma tarefa; paralelismo significa executar trabalho simultaneamente. Um programa pode alternar tarefas enquanto espera uma resposta sem executar cálculos Python em paralelo.
| Modelo | Funcionamento | O que avaliar |
|---|---|---|
| Threads | Compartilham memória dentro do processo. | Espera de entrada/saída, sincronização e comportamento das bibliotecas. |
| Processos | Usam espaços de memória separados. | Inicialização, serialização, cópia de dados e memória utilizada. |
| asyncio | Alterna tarefas cooperativamente em pontos de espera. | Bibliotecas assíncronas, bloqueio do loop e limites de tarefas. |
O GIL, Global Interpreter Lock, limita a execução simultânea de bytecode Python por threads em um CPython convencional com esse bloqueio habilitado. Nesse ambiente, adicionar threads não é uma solução automática para acelerar cálculo intensivo em Python puro. Extensões que liberam o GIL e builds sem GIL têm outras condições; confira o interpretador e as bibliotecas usados.
Como ponto de partida, avalie threads para operações bloqueantes de entrada/saída, processos para cálculo intensivo em Python puro e asyncio quando a biblioteca oferecer operações assíncronas. Nenhuma dessas escolhas elimina erros de rede, validação ou persistência. A aula de APIs HTTP estabelece esses limites antes de introduzir múltiplas requisições.
Executar o programa completo com três interfaces
Salve o código inteiro em concurrency.py, em UTF-8, numa pasta de prática. Ele usa somente a biblioteca padrão. No terminal do ambiente virtual do projeto, execute python concurrency.py; fora de um ambiente virtual no macOS ou Ubuntu, use python3 se esse for o nome do interpretador. Execute como arquivo .py: o pool de processos não tem as mesmas condições de importação em todos os notebooks e terminais interativos.
import asyncio
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def square(value):
return value * value
async def async_square(value):
await asyncio.sleep(0.01)
return square(value)
async def gather_values():
return await asyncio.gather(*(async_square(x) for x in [1, 2, 3]))
def main():
with ThreadPoolExecutor(max_workers=2) as pool:
print(list(pool.map(square, [1, 2, 3])))
with ProcessPoolExecutor(max_workers=2) as pool:
print(list(pool.map(square, [1, 2, 3])))
print(asyncio.run(gather_values()))
if __name__ == "__main__":
main()
Resultado esperado:
[1, 4, 9]
[1, 4, 9]
[1, 4, 9]
Cada linha contém os quadrados de 1, 2 e 3, na ordem dos valores de entrada. pool.map entrega os resultados nessa ordem; isso não prova que os trabalhos terminaram nessa sequência. asyncio.gather também organiza sua lista de resultados na ordem dos argumentos.
A função square fica no nível superior do módulo para poder ser localizada pelos processos de trabalho. A proteção if __name__ == "__main__" impede que a importação do módulo durante a criação de processos execute novamente main(). Isso é especialmente visível no Windows; não retire a proteção ao adaptar o arquivo.
O asyncio.sleep(0.01) simula espera e permite que o loop avance em outras tarefas. Ele não consulta uma API. Escrever async def também não torna uma operação bloqueante assíncrona: um cálculo longo ou uma chamada de rede síncrona pode impedir o progresso das demais tarefas no mesmo loop.
Observar cancelamento, exceções e limites de espera
Salve o próximo exemplo completo em cancel.py e execute python cancel.py na mesma pasta. A espera longa torna o cancelamento observável; o programa não precisa aguardar 60 segundos para terminar.
import asyncio
async def worker():
try:
await asyncio.sleep(60)
finally:
print("worker cleanup")
async def main():
task = asyncio.create_task(worker())
await asyncio.sleep(0)
task.cancel()
try:
await task
except asyncio.CancelledError:
print("cancel confirmed")
if __name__ == "__main__":
asyncio.run(main())
Saída esperada:
worker cleanup
cancel confirmed
O await asyncio.sleep(0) dá oportunidade para o trabalhador iniciar e entrar no try. task.cancel() solicita cancelamento; aguardar a tarefa permite observar sua conclusão. O finally imprime a limpeza antes de o chamador confirmar CancelledError. Não trate o pedido de cancelamento como encerramento já confirmado nem o use para esconder uma falha.
Para comparar uma falha em uma tarefa do pool, salve todo este código como future_failure.py e execute python future_failure.py:
from concurrent.futures import ThreadPoolExecutor
def calculate(value):
if value < 0:
raise ValueError("negative input")
return value * value
with ThreadPoolExecutor(max_workers=2) as pool:
futures = [pool.submit(calculate, value) for value in [2, -1]]
for future in futures:
try:
print(future.result(timeout=5))
except ValueError as error:
print("failed:", error)
O resultado esperado é:
4
failed: negative input
submit retorna um Future, objeto que representa um resultado pendente ou concluído. A exceção da função é observada ao chamar result. Aqui, ValueError é tratado individualmente, de modo que a falha do valor negativo aparece explicitamente.
max_workers=2 limita os trabalhadores, não estabelece por si só um limite de toda a fila de submissão. result(timeout=5) limita quanto essa chamada espera; não interrompe à força uma tarefa já em execução. A saída do with do executor aguarda os trabalhos pendentes. Para muitos dados, processe lotes limitados e planeje a espera e a interrupção de cada operação. No asyncio, use um limite explícito, como um semáforo, quando houver múltiplas operações de entrada/saída.
Consulte os contratos de executores, Future e encerramento e de cancelamento de tarefas asyncio ao adaptar as rotinas.
Testar mudanças e medir a carga relevante
- No primeiro arquivo, troque as três listas
[1, 2, 3]por[3, 1, 2]. As três linhas devem passar a[9, 1, 4]. Restaure as listas. - Troque somente
max_workers=2pormax_workers=1. Os resultados devem continuar iguais; isso não determina qual configuração será mais rápida numa aplicação real. Restaure o valor. - Em
future_failure.py, substitua apenas-1por3: as linhas devem ser4e9, sem a mensagem de falha. Restaure o valor negativo e confirme a mensagem original. - Execute
cancel.pyseparadamente e confira a ordem entre limpeza e confirmação. Mantenha oawait taskpara observar o resultado do cancelamento.
O cálculo dos quadrados é pequeno e serve para comparar interfaces. O custo de iniciar processos pode superar o próprio cálculo. Meça com dados representativos, registrando versão e build do Python, sistema, tamanho dos dados, trabalhadores, tempo e memória; não publique uma promessa de aceleração com base apenas nessas três linhas.
Threads exigem coordenação quando alteram estado compartilhado; processos não compartilham automaticamente uma lista comum; tarefas asyncio ainda podem disputar estado entre pontos de espera. Separe coleta, validação e escrita, como no projeto de API e SQLite, antes de paralelizar requisições ou gravações.
Se aparecer erro de loop já em execução num notebook, não acrescente outro asyncio.run dentro desse loop. O arquivo desta aula é um programa independente. No ambiente que já mantém um loop, use a integração de espera suportada por ele; a restrição está na documentação de asyncio.run.




