Trilha de aprendizado · Nível 13 · Tutorial 3

Controlar espera e encerramento de tarefas em executores

Ao concluir, você será capaz de limitar esperas, cancelar trabalho ainda não iniciado e solicitar a parada cooperativa de funções executadas em threads.

  • Nível: Avançado
  • Duração: 22 min
  • 8 passos
Controlar espera e encerramento de tarefas em executores

O que você vai percorrer

  1. Interpretar o estado de um futuro Use consultas de estado para observar se um futuro está em execução, finalizado ou efetivamente cancelado, sem confundir finalização com sucesso. 2 min
  2. Limitar a espera por um resultado Use um prazo em Future.result() sem confundir o fim da espera do chamador com o fim do trabalho na thread. 3 min
  3. Separar concluídos e não concluídos com wait Use wait para observar um lote de futuros sem confundir o fim da espera com o fim de todo o trabalho. 3 min
  4. Cancelar trabalho que ainda não começou Use o retorno de cancel() para saber se uma tarefa foi efetivamente cancelada e trate futuros cancelados ao recuperar resultados. 3 min
  5. Escolher como encerrar o executor Defina se o chamador espera, se a fila pendente será cancelada e o que continua acontecendo com tarefas já iniciadas. 3 min
  6. Solicitar parada cooperativa com Event Use um sinal compartilhado para que trabalhadores em threads encerrem seu próprio trabalho de forma cooperativa. 3 min
  7. Manter o trabalhador capaz de observar a parada Ajuste pontos de espera para que um trabalhador possa voltar a verificar um pedido cooperativo de parada. 3 min
  8. Aplicar uma política completa de encerramento Integre espera limitada, cancelamento da fila, parada cooperativa e inspeção final dos futuros em um lote finito. 4 min

O que você vai aprender

  • Aplicar tempos de espera sem confundi-los com a interrupção da função executada.
  • Separar futuros concluídos de futuros ainda pendentes com wait.
  • Interpretar o resultado de cancel e tratar futuros cancelados.
  • Implementar uma solicitação de parada cooperativa com threading.Event.
  • Explicar o que permanece em execução após shutdown ou após a saída por timeout.

Antes de começar

  • Executar tarefas com ThreadPoolExecutor

Passo 1 de 8

Interpretar o estado de um futuro

Use consultas de estado para observar se um futuro está em execução, finalizado ou efetivamente cancelado, sem confundir finalização com sucesso.

Estados observáveis de um Future

Uma consulta não conta toda a história

Um Future representa uma chamada submetida ao executor. Você pode observar seu estado com três métodos:

  • running() é True somente enquanto a função está sendo executada.
  • done() é True quando o futuro já terminou — com resultado, com exceção ou por cancelamento.
  • cancelled() é True somente quando o cancelamento foi efetivado antes de a função começar.

Assim, done() não significa “a função teve sucesso”.

Leitura dos estados

Observe como as consultas se combinam em cada situação.

Diagrama de estados de um Future: aguardando execução com running falso, done falso e cancelled falso; em execução com running verdadeiro, done falso e cancelled falso; concluído com resultado ou exceção com done verdadeiro; cancelado antes de executar com done e cancelled verdadeiros.

Um futuro concluído pode ter resultado, exceção ou cancelamento.

Consultar sem bloquear

Inspeção pontual de um futuro

Este exemplo usa uma pausa apenas para tornar o estado em execução fácil de observar. As consultas não esperam a conclusão.

python
from concurrent.futures import ThreadPoolExecutor
import time


def processar() -> str:
    time.sleep(1)
    return "pronto"


with ThreadPoolExecutor(max_workers=1) as executor:
    futuro = executor.submit(processar)

    print(futuro.running())    # pode ser True quando a função já iniciou
    print(futuro.done())       # False enquanto não finalizou
    print(futuro.cancelled())  # False: não houve cancelamento efetivo

    resultado = futuro.result()
    print(resultado)
    print(futuro.done())       # True após a finalização

Dica

Estado pode mudar entre linhas

Esses métodos são observações momentâneas. Entre futuro.running() e a próxima linha, a função pode terminar. Por isso, não use uma consulta isolada como garantia de uma ação futura; trate o resultado ao recuperá-lo.

Finalizado não é sinônimo de sucesso

Resultado e exceção

Depois de done() retornar True, result() não precisa devolver um valor: se a função falhou, ele propaga a exceção produzida pela função. Um futuro cancelado também está finalizado, mas sua recuperação segue um tratamento próprio, visto mais adiante.

Associe a consulta à interpretação

Relacione cada observação à interpretação adequada.

Toque em um item e depois no par correspondente.

Passo 2 de 8

Limitar a espera por um resultado

Use um prazo em Future.result() sem confundir o fim da espera do chamador com o fim do trabalho na thread.

Um prazo para esta espera

Timeout limita quem está esperando

Ao chamar futuro.result(timeout=segundos), o prazo limita apenas quanto tempo essa chamada aguardará pelo resultado.

Se o resultado não estiver disponível dentro do prazo, result() lança TimeoutError. Isso não equivale a parar a função submetida: o futuro pode continuar em execução e ficar pronto depois.

Dois fluxos independentes

O chamador deixa de esperar ao atingir o prazo; a thread trabalhadora segue sua função até ela terminar.

Linha do tempo com o chamador parando sua espera no limite de timeout e um trabalhador continuando até produzir um resultado mais tarde.

O timeout encerra a espera de result(), não a execução já iniciada da função.

Atenção

Não conclua demais

Após um TimeoutError, não suponha que a tarefa falhou, terminou ou teve seus efeitos desfeitos. O único fato conhecido é: o resultado não ficou disponível dentro do prazo escolhido para aquela chamada.

Trate o prazo e consulte depois

O mesmo futuro pode fornecer o resultado depois

Execute este script localmente. A pausa é apenas uma tarefa simulada para tornar a linha do tempo observável.

python
from concurrent.futures import ThreadPoolExecutor, TimeoutError
from time import sleep


def preparar_relatorio() -> str:
    sleep(2)
    return "relatório pronto"


with ThreadPoolExecutor(max_workers=1) as executor:
    futuro = executor.submit(preparar_relatorio)

    try:
        print(futuro.result(timeout=0.2))
    except TimeoutError:
        print("A espera venceu; o trabalhador pode continuar.")

    print("Concluído neste instante?", futuro.done())
    print("Resultado posterior:", futuro.result())

Exemplo

Leitura esperada

A primeira chamada a result(timeout=0.2) tende a lançar TimeoutError, porque a função simulada leva mais tempo. Depois, a segunda chamada futuro.result() aguarda o mesmo futuro e obtém "relatório pronto".

O except permite ao chamador decidir o que fazer enquanto o trabalho continua. Nesta etapa, não estamos cancelando nem encerrando o executor de forma especial.

Verifique sua previsão

Depois do TimeoutError

Uma função já está em execução em uma thread. futuro.result(timeout=0.1) lança TimeoutError. Qual afirmação é correta?

Passo 3 de 8

Separar concluídos e não concluídos com wait

Use wait para observar um lote de futuros sem confundir o fim da espera com o fim de todo o trabalho.

Uma fotografia do lote

O retorno de wait

concurrent.futures.wait(futuros, ...) recebe uma coleção de futuros e devolve dois conjuntos: done e not_done.

  • done: futuros que já terminaram, inclusive os que falharam com exceção ou foram cancelados.
  • not_done: futuros que ainda não terminaram. Eles podem estar aguardando uma thread na fila ou já estar em execução.

Essa divisão é uma fotografia do estado no instante em que wait retorna. Conjuntos não preservam a ordem em que você submeteu as tarefas.

Leitura dos conjuntos

O resultado separa finalizados de ainda não finalizados — não separa sucesso de falha.

Diagrama de um lote de cinco futuros dividido em dois conjuntos: done contém um futuro com resultado, um com exceção e um cancelado; not_done contém um futuro em execução e outro aguardando na fila.

done inclui resultado, exceção e cancelamento; not_done pode combinar fila e execução.

Quando wait retorna

Condição e prazo

Por padrão, wait usa return_when=ALL_COMPLETED: só retorna quando todos os futuros terminarem.

Com return_when=FIRST_COMPLETED, retorna assim que pelo menos um futuro termina ou é cancelado. Isso não quer dizer “primeiro resultado bem-sucedido” e também não garante que done terá exatamente um futuro: outros podem ter terminado antes de a chamada devolver o controle.

Você também pode passar timeout. Ao prazo se esgotar, wait simplesmente devolve os conjuntos disponíveis naquele momento. Diferentemente de Future.result(timeout=...), isso não lança TimeoutError e não cancela o trabalho que restou.

Esperar até haver alguma conclusão

O código examina apenas os futuros que já pertencem a done.

python
from concurrent.futures import FIRST_COMPLETED, wait

# futuros foi criado anteriormente com executor.submit(...)
done, not_done = wait(
    futuros,
    timeout=2,
    return_when=FIRST_COMPLETED,
)

print(f"finalizados agora: {len(done)}")
print(f"ainda não finalizados: {len(not_done)}")

for futuro in done:
    try:
        print("resultado:", futuro.result())
    except Exception as erro:
        print("a tarefa terminou com erro:", erro)

# Os futuros em not_done continuam na fila ou em execução.

Dica

Observe somente o que terminou

Chamar futuro.result() para cada elemento de done não cria uma nova espera pela conclusão daquele futuro. Ainda assim, o método pode propagar a exceção produzida pela função; por isso, trate-a ao observar cada resultado.

Escolha a chamada adequada

Condição de retorno

Complete a constante para que wait retorne quando pelo menos um futuro do lote terminar ou for cancelado:

done, not_done = wait(futuros, return_when=_____)

Interprete a separação

O que pode estar em not_done?

Você chama wait(futuros, timeout=1) e recebe alguns futuros em done e outros em not_done. Explique o que os elementos de not_done podem estar fazendo e o que o timeout não fez com eles.

Escreva pelo menos 60 caracteres (0/60).

Passo 4 de 8

Cancelar trabalho que ainda não começou

Use o retorno de cancel() para saber se uma tarefa foi efetivamente cancelada e trate futuros cancelados ao recuperar resultados.

O que cancel() realmente faz

Cancelamento não é interrupção

Future.cancel() tenta cancelar um futuro antes de sua função começar a executar. Se o trabalho já estiver em andamento, a chamada não interrompe a thread nem força a função a parar.

Use o valor retornado pela própria chamada como decisão:

  • True: o futuro foi cancelado — ou já estava cancelado.
  • False: a função está em execução ou o futuro já terminou sem cancelamento.

Uma consulta como future.running() é apenas uma observação momentânea. Entre essa consulta e future.cancel(), o executor pode iniciar a tarefa.

Duas tentativas de cancelamento

O momento em que cancel() é chamado define se a tentativa pode impedir o início da função.

Diagrama comparando um futuro na fila que é cancelado com sucesso e um futuro em execução cujo cancelamento falha, enquanto a função continua.

Na fila, cancel() pode retornar True. Depois do início da execução, retorna False e o trabalho segue.

Verifique o resultado da tentativa

Uma tarefa em execução não é interrompida

Execute este exemplo localmente. A tarefa primeira ocupa o único trabalhador; por isso, segunda ainda está na fila quando tentamos cancelá-la.

python
from concurrent.futures import ThreadPoolExecutor, CancelledError
import time


def trabalho(nome: str, demora: float) -> str:
    print(f"{nome}: iniciou")
    time.sleep(demora)
    print(f"{nome}: terminou")
    return nome


with ThreadPoolExecutor(max_workers=1) as executor:
    primeira = executor.submit(trabalho, "primeira", 0.3)
    segunda = executor.submit(trabalho, "segunda", 0.1)

    foi_cancelada = segunda.cancel()
    print(f"cancel() retornou: {foi_cancelada}")
    print(f"done={segunda.done()}, cancelled={segunda.cancelled()}")

    try:
        print("resultado:", segunda.result())
    except CancelledError:
        print("segunda foi cancelada; não há resultado para recuperar")

    print("primeira:", primeira.result())

Após um cancelamento efetivo

Um futuro cancelado está finalizado: done() e cancelled() retornam True. Porém, ele não possui um valor de retorno.

Assim, future.result() lança concurrent.futures.CancelledError. Trate essa exceção separadamente quando o cancelamento for um resultado esperado da sua política de coordenação.

Teste sua interpretação

not_done e cancelamento

Se um futuro aparece em not_done após wait, então uma chamada imediata a cancel() certamente retornará True.

Decida pelo retorno de cancel()

Cenário de corrida de estado

Você chama futuro.running() e recebe False. Logo depois, chama futuro.cancel(), que retorna False. Por que isso é possível? O que seu código deve usar para decidir se precisa tratar CancelledError?

Escreva pelo menos 80 caracteres (0/80).

Passo 5 de 8

Escolher como encerrar o executor

Defina se o chamador espera, se a fila pendente será cancelada e o que continua acontecendo com tarefas já iniciadas.

O que shutdown decide

Fechar não é interromper tudo

executor.shutdown(...) impede novas submissões. Depois dessa chamada, executor.submit(...) falha com RuntimeError.

A política de encerramento tem duas decisões independentes:

  • wait: o chamador espera ou retoma o controle agora?
  • cancel_futures: itens que ainda estão na fila devem ser cancelados?

Nenhuma dessas opções interrompe uma função que já está em execução.

Duas decisões, três destinos

A configuração afeta de formas diferentes quem chamou o executor, as tarefas na fila e as tarefas já em execução.

Diagrama de um executor fechado para novas submissões, com um chamador, tarefas pendentes em uma fila e duas tarefas em execução; setas distinguem esperar, cancelar pendentes e permitir que tarefas ativas terminem.

wait controla a espera do chamador; cancel_futures alcança apenas a fila. Trabalho ativo continua até a própria função terminar.

Matriz de políticas

Combine as opções conforme a política

| Chamada | Chamador | Trabalho na fila | Trabalho em execução |
| --- | --- | --- | --- |
| shutdown(wait=True, cancel_futures=False) | espera | continua e termina | termina |
| shutdown(wait=False, cancel_futures=False) | retorna logo | continua e termina | termina |
| shutdown(wait=True, cancel_futures=True) | espera | é cancelado se ainda não iniciou | termina |
| shutdown(wait=False, cancel_futures=True) | retorna logo | é cancelado se ainda não iniciou | termina |

cancel_futures vale False por padrão. Mesmo com wait=False, as threads não são eliminadas: o interpretador ainda aguarda o trabalho pendente não cancelado antes de encerrar o programa.

Atenção

Retornar não significa que o programa acabou

shutdown(wait=False) devolve o controle ao seu código sem esperar. Isso não é uma forma de forçar a saída do processo nem de matar threads. Não feche recursos que as funções ainda usam só porque a chamada retornou.

O contexto with sempre espera

Timeout dentro do contexto

Execute este exemplo para observar a ordem dos eventos.

python
from concurrent.futures import ThreadPoolExecutor, TimeoutError
from time import sleep


def tarefa_lenta() -> str:
    print("trabalhador: começou")
    sleep(1)
    print("trabalhador: terminou")
    return "pronto"


try:
    with ThreadPoolExecutor(max_workers=1) as executor:
        futuro = executor.submit(tarefa_lenta)
        print(futuro.result(timeout=0.1))
except TimeoutError:
    print("chamador: timeout tratado fora do with")

print("fim do programa")

Leia a ordem, não os tempos exatos

O result(timeout=0.1) lança TimeoutError, mas a saída do with executa o equivalente a shutdown(wait=True). Portanto, o trabalhador termina antes de o except externo receber a exceção e imprimir sua mensagem.

O timeout limitou apenas aquela espera por resultado; ele não limitou a duração total do bloco with. Além disso, a saída do contexto não cancela automaticamente o que estava pendente.

Verifique a política

Associe a configuração ao efeito principal

Relacione cada situação à consequência correta.

Toque em um item e depois no par correspondente.

Explique o caso

Por que um TimeoutError gerado por futuro.result(timeout=...) dentro de um bloco with pode fazer o except externo esperar mais tempo antes de executar?

Escreva pelo menos 80 caracteres (0/80).

Passo 6 de 8

Solicitar parada cooperativa com Event

Use um sinal compartilhado para que trabalhadores em threads encerrem seu próprio trabalho de forma cooperativa.

Um sinal compartilhado, não uma interrupção forçada

O papel do Event

threading.Event é um sinal compartilhado entre quem coordena o trabalho e os trabalhadores. Um Event() novo começa desativado.

  • O coordenador chama parar.set() para ativar o pedido de parada.
  • Cada trabalhador consulta parar.is_set() entre unidades de trabalho.
  • Ao perceber o sinal, a própria função retorna voluntariamente.

A mesma instância de Event deve ser passada a todos os trabalhadores que precisam responder ao mesmo pedido.

Fluxo da parada cooperativa

Diagrama mostrando um coordenador ativando um Event compartilhado, dois trabalhadores consultando o sinal entre etapas de um laço e saindo voluntariamente após observá-lo.

O coordenador sinaliza uma vez; cada trabalhador decide encerrar quando volta a consultar o mesmo Event.

Dica

Não confunda os mecanismos

Chamar set() não marca o futuro como cancelado e não encerra uma thread à força. É um pedido que só produz efeito quando o código do trabalhador coopera e observa o sinal.

Consultar ou aguardar o sinal

Duas formas de observar

Use is_set() para uma consulta imediata: ele retorna True se o sinal já está ativo.

Use wait(timeout) quando o trabalhador pode aguardar o sinal por até um prazo. Ele retorna True se o Event for ativado durante a espera e False se o prazo terminar sem sinalização. Isso permite decidir se continua para a próxima unidade ou se retorna.

Padrão dentro de um laço

O retorno após detectar o sinal é uma conclusão normal da função.

python
def trabalhador(parar):
    for unidade in range(1, 11):
        if parar.is_set():
            return "parada cooperativa antes da próxima unidade"

        processar_unidade(unidade)

        recebeu_sinal = parar.wait(0.2)
        if recebeu_sinal:
            return "parada cooperativa após a unidade"

    return "trabalho concluído"

Exemplo

Leitura do retorno de wait

Se parar.wait(0.2) retornar False, nenhum sinal foi ativado nesses 0,2 segundo e o laço pode continuar. Se retornar True, o coordenador ativou o Event e o trabalhador deve seguir sua política de encerramento, como retornar após liberar recursos.

Prática local: sinalize e observe o resultado

Execute no seu computador

Crie um arquivo Python com o código abaixo e execute-o. O coordenador solicita a parada após um curto intervalo. Não espere uma quantidade exata de unidades impressas: o agendamento das threads pode variar.

Observe que o trabalhador executa sua limpeza em finally e que o valor recuperado por result() é o retorno normal da função.

Programa completo

python
from concurrent.futures import ThreadPoolExecutor
from threading import Event
import time


def trabalhador(nome: str, parar: Event) -> str:
    try:
        for unidade in range(1, 21):
            if parar.is_set():
                return f"{nome}: parada cooperativa antes da unidade {unidade}"

            print(f"{nome}: processando unidade {unidade}")

            # Aguarda no máximo este intervalo; retorna antes se receber o sinal.
            if parar.wait(0.15):
                return f"{nome}: parada cooperativa após a unidade {unidade}"

        return f"{nome}: concluiu todas as unidades"
    finally:
        print(f"{nome}: limpeza do trabalhador")


parar = Event()

with ThreadPoolExecutor(max_workers=1) as executor:
    futuro = executor.submit(trabalhador, "T1", parar)
    try:
        time.sleep(0.35)
        print("Coordenador: solicitando parada")
        parar.set()
        resultado = futuro.result()
    finally:
        # Garante a sinalização mesmo se o coordenador sair por uma exceção.
        parar.set()

print("Resultado:", resultado)
print("done():", futuro.done())
print("cancelled():", futuro.cancelled())

Dica

O que verificar na saída

A quantidade de mensagens de processamento não é fixa. O ponto importante é que, após a solicitação, o futuro termina com um texto retornado pelo trabalhador; em seguida, done() é True e cancelled() é False.

Comprove a distinção

Ative o pedido de parada

Para solicitar cooperativamente a parada aos trabalhadores, o coordenador chama parar.____().

Interprete o futuro

Após executar o programa, explique por que o futuro do trabalhador pode ter done() igual a True e cancelled() igual a False, mesmo depois de o coordenador solicitar a parada.

Escreva pelo menos 80 caracteres (0/80).

Passo 7 de 8

Manter o trabalhador capaz de observar a parada

Ajuste pontos de espera para que um trabalhador possa voltar a verificar um pedido cooperativo de parada.

O sinal não interrompe qualquer bloqueio

A oportunidade de cooperar

Um trabalhador só reage ao pedido de parada quando seu fluxo de execução volta a consultar o Event ou quando está esperando no próprio Event. Portanto, chamar parar.set() não interrompe à força uma função que esteja em uma operação bloqueante qualquer.

Se uma unidade de trabalho demora, há um bloqueio longo ou há limpeza pendente, a resposta pode demorar. Parada cooperativa não oferece prazo rígido de encerramento.

Do sinal à observação

Compare os dois tipos de espera antes de escolher onde posicionar a próxima verificação.

Diagrama comparando um trabalhador bloqueado em uma pausa comum, que só verifica o sinal ao terminar, com outro esperando no Event, que acorda após o sinal.

Uma pausa comum termina pelo prazo; Event.wait(timeout) pode terminar pelo prazo ou pela sinalização.

Atenção

Não confunda sinal com interrupção

Event.set() pode acordar uma thread que está em Event.wait(...). Ele não interrompe time.sleep(), uma chamada de entrada e saída, um cálculo demorado ou outro bloqueio arbitrário. Quando uma API bloqueante oferecer timeout próprio, use um prazo adequado para ela devolver o controle periodicamente.

Duas pausas, duas respostas

Troque a espera quando ela precisa reagir

Nos dois programas a seguir, o coordenador pede a parada depois de pouco tempo. Não compare números exatos: o agendamento varia. Observe apenas a diferença qualitativa: no primeiro caso, o trabalhador percebe o pedido depois de terminar sleep; no segundo, wait pode retornar assim que o sinal é ativado.

Programa A — pausa que não responde ao sinal

Execute este script localmente. Ele é finito e usa somente a biblioteca padrão.

python
import threading
import time
from concurrent.futures import ThreadPoolExecutor


def trabalhador(parar: threading.Event) -> str:
    print("trabalhador: iniciando pausa")
    time.sleep(0.6)
    if parar.is_set():
        return "parada percebida depois da pausa"
    return "pausa terminou sem pedido"


parar = threading.Event()
with ThreadPoolExecutor(max_workers=1) as executor:
    futuro = executor.submit(trabalhador, parar)
    time.sleep(0.2)
    print("coordenador: solicitando parada")
    parar.set()
    print(f"resultado: {futuro.result()}")

Programa B — espera sensível ao sinal

Agora execute esta versão e compare a ordem das mensagens.

python
import threading
import time
from concurrent.futures import ThreadPoolExecutor


def trabalhador(parar: threading.Event) -> str:
    print("trabalhador: aguardando sinal ou prazo")
    recebeu_sinal = parar.wait(timeout=0.6)
    if recebeu_sinal:
        return "parada percebida durante a espera"
    return "prazo da espera terminou"


parar = threading.Event()
with ThreadPoolExecutor(max_workers=1) as executor:
    futuro = executor.submit(trabalhador, parar)
    time.sleep(0.2)
    print("coordenador: solicitando parada")
    parar.set()
    print(f"resultado: {futuro.result()}")

Diagnosticar a demora

Qual ajuste é apropriado?

Um trabalhador repete unidades curtas, mas entre elas usa time.sleep(30) para aguardar. Ele deve responder mais cedo a um pedido de parada durante essa espera. Qual mudança preserva a ideia de uma espera de até 30 segundos?

Localize o ponto cego

Uma função chama uma operação bloqueante que não consulta o Event e pode esperar indefinidamente. Explique onde está o problema para a parada cooperativa e justifique uma adaptação geral, sem citar uma API específica.

Escreva pelo menos 80 caracteres (0/80).

Passo 8 de 8

Aplicar uma política completa de encerramento

Integre espera limitada, cancelamento da fila, parada cooperativa e inspeção final dos futuros em um lote finito.

Uma política, quatro responsabilidades

O timeout não encerra o lote

Em uma política de encerramento completa, o prazo de wait(...) define apenas quanto tempo o coordenador observará o lote inicialmente. Se ainda houver futuros em not_done, o coordenador solicita parada aos trabalhadores ativos com um Event e pede ao executor que cancele o que ainda estiver na fila.

O encerramento completo pode durar mais que esse prazo: uma função que já começou precisa voltar a observar o sinal e retornar por conta própria.

Do prazo ao encerramento real

A linha do tempo separa o retorno de wait do fim efetivo do trabalho.

Diagrama mostrando wait com timeout retornando enquanto duas tarefas ativas recebem um sinal de parada, tarefas na fila são canceladas e o shutdown aguarda as tarefas ativas terminarem.

Após o timeout, podem coexistir tarefas canceladas e tarefas ainda em execução cooperando para parar.

Dica

Não suponha uma ordem fixa

Entre a decisão de encerrar e shutdown(cancel_futures=True), uma tarefa da fila pode começar. Nesse caso, ela não poderá mais ser cancelada e dependerá do sinal cooperativo. Por isso, não baseie sua lógica em uma contagem exata de cancelamentos.

Prática local: lote finito e autocontido

Execute no seu computador

Crie um arquivo, por exemplo encerrar_lote.py, cole o código completo abaixo e execute com python encerrar_lote.py. Ele usa somente a biblioteca padrão. Os nomes e a ordem das mensagens podem variar entre execuções.

Script completo

python
from concurrent.futures import ThreadPoolExecutor, wait
from threading import Event


def trabalhar(nome: str, parar: Event, falhar: bool = False) -> str:
    for unidade in range(20):
        # A espera termina antes do prazo se alguém sinalizar parar.
        if parar.wait(timeout=0.1):
            return f"{nome}: parada cooperativamente na unidade {unidade}"

        if falhar and unidade == 2:
            raise RuntimeError(f"{nome}: falha simulada")

    return f"{nome}: concluída normalmente"


parar = Event()
executor = ThreadPoolExecutor(max_workers=3)
futuros = {
    executor.submit(trabalhar, "longa-1", parar): "longa-1",
    executor.submit(trabalhar, "longa-2", parar): "longa-2",
    executor.submit(trabalhar, "falha", parar, True): "falha",
    executor.submit(trabalhar, "fila-1", parar): "fila-1",
    executor.submit(trabalhar, "fila-2", parar): "fila-2",
}

try:
    done, not_done = wait(futuros, timeout=0.35)
    print(f"Após a espera inicial: {len(done)} concluído(s), "
          f"{len(not_done)} ainda não concluído(s).")

    if not_done:
        print("Solicitando parada cooperativa e cancelamento da fila...")
finally:
    # Também protege caminhos inesperados, não apenas o caminho de sucesso.
    parar.set()
    executor.shutdown(wait=True, cancel_futures=True)

print("\nEstado final:")
for futuro, nome in futuros.items():
    if futuro.cancelled():
        print(f"- {nome}: cancelada antes de iniciar")
        continue

    try:
        print(f"- {nome}: {futuro.result()}")
    except Exception as erro:
        print(f"- {nome}: exceção {type(erro).__name__}: {erro}")

Leia o resultado por estado, não por ordem

O que o script faz

wait(..., timeout=0.35) devolve os conjuntos disponíveis sem lançar TimeoutError. No finally, parar.set() solicita que tarefas iniciadas retornem, e shutdown(wait=True, cancel_futures=True) impede novos envios, tenta cancelar a fila e espera as tarefas não canceladas acabarem.

A inspeção ocorre somente depois do shutdown: cada futuro é classificado como cancelado, resultado normal — inclusive retorno por parada cooperativa — ou exceção da função.

Ordene a política de encerramento

Organize a sequência aplicada quando ainda existe trabalho após a observação inicial.

  1. `wait` retorna após o prazo com os conjuntos disponíveis
  2. Chamar `shutdown(wait=True, cancel_futures=True)`
  3. Sinalizar `Event` para solicitar a parada das tarefas ativas
  4. Inspecionar cada futuro após o encerramento

Revisão e aplicação final

Interprete sua execução

Com base na sua execução, explique: o que o timeout limitou, quais tarefas poderiam ser canceladas e por que o programa pode continuar aguardando depois desse prazo.

Escreva pelo menos 120 caracteres (0/120).

Resumo

Política integrada

  • wait(..., timeout=...) limita a espera do coordenador e devolve done e not_done; não encerra o trabalho restante.
  • Event.set() é um pedido cooperativo: só tarefas que voltam a consultar o sinal podem parar por esse mecanismo.
  • shutdown(cancel_futures=True) cancela apenas tarefas que ainda não começaram; uma tarefa que iniciou nesse intervalo precisa cooperar.
  • shutdown(wait=True) aguarda tarefas não canceladas, portanto o encerramento total pode ultrapassar a espera inicial.
  • Após o encerramento, use cancelled() antes de result() para distinguir cancelamento, retornos e exceções.

Tutorial concluído

Parabéns! Você concluiu: Controlar espera e encerramento de tarefas em executores

Você integrou limite de espera, cancelamento da fila, parada cooperativa e encerramento responsável de um executor.

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso