Aplicações Python que usam múltiplos processos normalmente encerram o pool de maneira cooperativa, esperando as tarefas terminarem e liberando os recursos com calma. Porém, há situações em que isso não basta: um worker pode travar, ficar preso em uma extensão nativa, consumir memória sem controle ou impedir o desligamento da aplicação. Para esses casos, versões recentes do Python adicionaram os métodos terminate_workers() e kill_workers() ao ProcessPoolExecutor. Eles permitem interromper os processos trabalhadores de forma explícita e previsível.
Neste guia, você aprenderá a diferença entre terminar e matar workers, quando usar cada opção, como tratar tarefas pendentes, quais exceções esperar e como estruturar um encerramento seguro sem deixar processos órfãos.
O problema de encerrar pools travados
O ProcessPoolExecutor facilita a execução paralela de funções em processos separados. Em condições normais, o bloco with chama shutdown() ao sair e aguarda o encerramento dos workers. Isso funciona bem quando as tarefas respondem, finalizam ou levantam exceções normalmente.
O problema aparece quando uma tarefa entra em loop infinito, trava em código externo ou fica bloqueada em uma operação que não retorna. Nesses casos, esperar indefinidamente não é aceitável para serviços, pipelines, filas ou rotinas de manutenção. O processo principal precisa de um mecanismo de emergência.
O tema complementa outros conteúdos da Academify sobre concorrência, como InterpreterPoolExecutor no Python, os.process_cpu_count no Python, asyncio.Queue.shutdown no Python e queue.SimpleQueue no Python.
terminate_workers e kill_workers
Os dois métodos encerram imediatamente os workers existentes e também executam internamente o processo de desligamento do executor. A diferença principal está no sinal usado pelo sistema operacional.
terminate_workers()chama o equivalente aProcess.terminate().kill_workers()chama o equivalente aProcess.kill().
Em sistemas POSIX, terminar costuma enviar SIGTERM, enquanto matar normalmente envia SIGKILL. O primeiro oferece uma chance limitada de reação ao processo; o segundo força o encerramento e não pode ser tratado pelo worker. No Windows, os detalhes internos diferem, mas a ideia de uma interrupção mais forte continua válida.
from concurrent.futures import ProcessPoolExecutor
executor = ProcessPoolExecutor(max_workers=4)
try:
futures = [executor.submit(processar, item) for item in itens]
# processamento normal
except TimeoutError:
executor.terminate_workers()
Depois de chamar qualquer um desses métodos, não envie novas tarefas ao executor. Ele deve ser considerado encerrado.
Quando usar terminate_workers
Prefira terminate_workers() como primeira etapa quando você precisa interromper o pool, mas ainda quer adotar a alternativa menos agressiva. Ele é apropriado quando os workers estão demorando demais, quando o serviço está entrando em desligamento ou quando um limite operacional foi excedido.
Mesmo assim, não dependa de blocos finally dentro do worker para garantir consistência. Um processo terminado pode não concluir gravações, buffers ou transações. Recursos externos, como banco de dados e arquivos, devem ser protegidos por transações, operações idempotentes e checkpoints.
Quando usar kill_workers
kill_workers() é a opção de último recurso. Use quando os processos não respondem à terminação, estão presos em código nativo ou precisam desaparecer imediatamente para proteger a máquina. Um exemplo seria um worker consumindo memória rapidamente ou impedindo a reinicialização de um serviço crítico.
executor.terminate_workers()
# Se a aplicação ainda detectar processos resistentes,
# um novo pool ou uma camada supervisora pode aplicar kill.
Na prática, a aplicação deve registrar por que a intervenção extrema ocorreu. Matar processos sem observabilidade dificulta encontrar a causa raiz.
Tarefas pendentes e futures
Quando os workers são encerrados abruptamente, tarefas em execução não concluem normalmente. Os objetos Future podem terminar com exceções relacionadas à quebra do pool, como BrokenProcessPool. Tarefas ainda não iniciadas também podem ser canceladas ou falhar conforme o estado do executor.
from concurrent.futures.process import BrokenProcessPool
for future in futures:
try:
resultado = future.result()
except BrokenProcessPool:
registrar_falha("pool encerrado abruptamente")
except Exception as exc:
registrar_falha(str(exc))
Não presuma que uma tarefa interrompida pode ser simplesmente repetida. Antes de reenviar, verifique se ela já produziu efeitos parciais. Operações de pagamento, envio de mensagens e alterações em banco exigem chaves de idempotência.
Encerramento com timeout
Uma estratégia robusta combina timeout, tentativa cooperativa e escalonamento. Primeiro, aguarde um prazo razoável. Depois, tente cancelar futures que ainda não começaram. Em seguida, termine os workers. Só use encerramento forçado se a aplicação continuar presa.
from concurrent.futures import wait
concluidas, pendentes = wait(futures, timeout=30)
if pendentes:
for future in pendentes:
future.cancel()
executor.terminate_workers()
Esse desenho evita aplicar uma medida extrema cedo demais. Também separa tarefas lentas de tarefas realmente travadas.
Não reutilize o executor
Após terminate_workers() ou kill_workers(), o executor não pode voltar ao estado saudável. Crie uma nova instância caso precise continuar processando.
executor.terminate_workers()
executor = ProcessPoolExecutor(max_workers=2)
A recriação deve acontecer apenas depois de entender se a causa do travamento continua presente. Caso contrário, a aplicação pode entrar em um ciclo infinito de criar e destruir pools.
Cuidados com arquivos e bancos
Processos interrompidos podem deixar arquivos temporários, locks externos e registros incompletos. Prefira gravar em arquivo temporário e usar renomeação atômica ao final. Em bancos, use transações curtas e confirme somente depois que o resultado estiver completo.
Também evite compartilhar estado mutável complexo entre workers. Filas, arquivos e serviços externos devem ter protocolos claros de retomada. O processo principal precisa ser capaz de identificar itens não concluídos.
Compatibilidade entre versões
Como os métodos são recentes, projetos que suportam versões anteriores do Python devem detectar sua existência. Uma abordagem simples é usar hasattr e manter um fallback.
if hasattr(executor, "terminate_workers"):
executor.terminate_workers()
else:
executor.shutdown(wait=False, cancel_futures=True)
O fallback não é idêntico: shutdown(wait=False) não garante a interrupção imediata de processos bloqueados. Por isso, documente a diferença de comportamento.
Supervisão externa
Para sistemas críticos, não confie apenas no processo Python para supervisionar a si mesmo. Systemd, Kubernetes, Docker ou outro supervisor podem aplicar limites de memória, reiniciar o serviço e encerrar todo o grupo de processos se necessário.
O controle interno é útil para recuperar tarefas e registrar contexto. A supervisão externa protege a máquina quando o próprio processo principal também está comprometido.
Observabilidade e diagnóstico
Antes de encerrar o pool, registre o número de tarefas pendentes, duração, tipo de trabalho e identificadores correlacionáveis. Depois, registre qual método foi usado. Métricas de tempo de execução e quantidade de reinicializações ajudam a detectar regressões.
Para investigar travamentos em processos em execução, consulte também pdb -p no Python. A documentação oficial de concurrent.futures descreve o contrato do executor, enquanto a documentação de multiprocessing explica os efeitos de terminate e kill.
Boas práticas finais
- Use timeouts em operações potencialmente bloqueantes.
- Projete tarefas idempotentes e reiniciáveis.
- Prefira
terminate_workers()antes dekill_workers(). - Não reutilize um executor encerrado.
- Trate
BrokenProcessPoole estados parciais. - Combine controle interno com supervisão externa.
terminate_workers() e kill_workers() dão ao ProcessPoolExecutor uma saída clara para situações em que o desligamento tradicional não funciona. Eles não substituem um bom desenho de concorrência, mas tornam a recuperação de falhas mais explícita. Usados com timeout, idempotência, logs e supervisão, ajudam a impedir que um único worker travado paralise toda a aplicação.







