CodeGym /Cursos /JAVA 25 SELF /Cancelamento de tarefas e timeouts ao longo da pilha

Cancelamento de tarefas e timeouts ao longo da pilha

JAVA 25 SELF
Nível 58 , Lição 1
Disponível

1. Thread.interrupt() e cancelamento cooperativo

Em aplicações reais, as tarefas podem ser longas e às vezes até “travar” — ao trabalhar com rede, arquivos ou serviços externos. O usuário pode cancelar a operação, o servidor pode interromper o processamento da requisição ou simplesmente o timeout geral pode expirar. Se você não souber cancelar tarefas corretamente, o aplicativo ficará travado, gastará recursos à toa e reagirá mal a eventos externos.

Ideia‑chave: o cancelamento deve ser cooperativo — a própria tarefa deve verificar se foi solicitada a finalizar e liberar recursos corretamente.

Como funciona Thread.interrupt()

Cada thread tem um sinalizador de interrupção. Quando você chama thread.interrupt(), esse sinalizador é definido como true. A thread não é “morta” automaticamente; ela deve verificar seu próprio status e encerrar-se: chamar periodicamente Thread.currentThread().isInterrupted() e sair corretamente.

Exemplo:

Thread worker = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // Trabalhando...
        try {
            Thread.sleep(100); // Pode ser interrompido
        } catch (InterruptedException e) {
            // O sinalizador é limpo, mas podemos nos interromper novamente
            Thread.currentThread().interrupt();
            break;
        }
    }
    System.out.println("Thread finalizada por interrupção.");
});
worker.start();

// ... mais tarde
worker.interrupt();

Onde o sinalizador funciona automaticamente?

  • Métodos que podem bloquear (sleep, wait, join, operações de estruturas bloqueantes) lançam InterruptedException ao serem interrompidos.
  • Nos demais casos (por exemplo, em um laço computacional) é preciso verificar manualmente isInterrupted().

Padrão “defina o sinalizador — e saia rapidamente”

  1. No código chamador: thread.interrupt()
  2. Na tarefa: verificar periodicamente Thread.currentThread().isInterrupted()
  3. Se necessário — liberar recursos corretamente e encerrar.

Erro típico: esperar que interrupt() “mate” a thread instantaneamente. Não — é apenas um sinal; a tarefa deve reagir por conta própria.

2. Future.cancel(), CancellationException e cancelamento de tarefas

Como funciona Future.cancel

Quando você executa uma tarefa via ExecutorService.submit(), recebe um objeto Future. Ele possui o método cancel(boolean mayInterruptIfRunning):

  • Se a tarefa ainda não começou — ela não será iniciada.
  • Se a tarefa já estiver em execução e mayInterruptIfRunning == true — será chamado interrupt() na thread que executa a tarefa.
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // Trabalho demorado
    }
    System.out.println("Tarefa finalizada por cancelamento.");
});

// ... mais tarde
future.cancel(true); // Solicitar cancelamento da tarefa

O que realmente acontece com a tarefa

O cancelamento via Future não é um botão mágico de “matar thread”; é, na prática, uma forma educada de Thread.interrupt(). Se a tarefa verifica o sinalizador de interrupção corretamente — ela termina de forma adequada. Caso contrário — continuará trabalhando até seu término natural.

Se você chamar future.get() após o cancelamento, receberá CancellationException — um lembrete de que a tarefa foi cancelada.

3. CompletableFuture: cancelamento, timeouts e cadeias

Cancelamento em CompletableFuture

CompletableFuture também possui cancel(boolean). Se a tarefa ainda não terminou, ela será cancelada e todos os manipuladores subsequentes (thenApply, thenAccept etc.) não serão chamados.

CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // Trabalhando...
    }
    System.out.println("CF finalizado por cancelamento.");
});

// ... mais tarde
cf.cancel(true);

Timeouts: orTimeout e completeOnTimeout

  • orTimeout(timeout, unit) — finaliza o CompletableFuture com TimeoutException se ele não terminar a tempo.
  • completeOnTimeout(value, timeout, unit) — finaliza com o valor especificado, se não terminar a tempo.
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
    try { Thread.sleep(5000); } catch (InterruptedException e) {}
    return "OK";
});

cf.orTimeout(2, TimeUnit.SECONDS)
  .exceptionally(ex -> "TIMEOUT")
  .thenAccept(System.out::println); // Em 2 segundos: "TIMEOUT"

Propagação do cancelamento em cadeias

Se você cancelar o CompletableFuture “superior”, todas as etapas subsequentes na cadeia não serão chamadas. Mas ao usar thenCompose para iniciar operações assíncronas internas, o cancelamento não é propagado automaticamente “para cima” — é preciso projetá-lo explicitamente (verificar status, cancelar tarefas filhas, usar um deadline comum).

Cuidado com thenCompose e Executor personalizado! Garanta que as tarefas internas saibam reagir a interrupção/cancelamento e/ou recebam um timeout comum.

4. StructuredTaskScope: cancelamento de grupo de tarefas

Concorrência estruturada e cancelamento

StructuredTaskScope (Java 21+) permite iniciar um grupo de tarefas e gerenciar seu ciclo de vida como um todo. Se uma das tarefas terminar com erro ou o timeout expirar — as demais tarefas serão automaticamente canceladas.

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> f1 = scope.fork(() -> fetchData1());
    Future<String> f2 = scope.fork(() -> fetchData2());

    scope.join(); // aguardamos a conclusão de todas as tarefas
    scope.throwIfFailed(); // se alguma falhou — lançamos exceção

    String result = f1.resultNow() + f2.resultNow();
    System.out.println(result);
}
  • Se qualquer tarefa terminar com erro — o scope cancela todas as demais tarefas.
  • Se o timeout expirar (via scope.joinUntil(deadline)) — o scope cancela todas as tarefas.

Políticas de término

  • ShutdownOnFailure — cancela todas as tarefas na primeira falha.
  • ShutdownOnSuccess — cancela as demais tarefas assim que uma terminar com sucesso.

5. Prática: cancelamento seguro de operações longas

Exemplo: cancelamento de IO bloqueante

Se a tarefa está bloqueada lendo de um arquivo ou da rede, interromper a thread nem sempre ajuda — algumas operações de IO não reagem a interrupt. Em APIs modernas (NIO, AsynchronousFileChannel) a interrupção é melhor suportada, mas ainda não em todos os lugares.

Recomendações:

  • Use IO não bloqueante se precisar de cancelamento.
  • Para IO bloqueante — defina timeouts no nível da API (por exemplo, Socket.setSoTimeout).
  • Para tarefas assíncronas — use Future.cancel e reaja corretamente à interrupção.

Exemplo: cancelamento de espera em fila/barreira

Muitos sincronizadores (BlockingQueue.take(), CountDownLatch.await(), CyclicBarrier.await()) lançam InterruptedException quando interrompidos. No tratador, capture a exceção, restaure o sinalizador se necessário e finalize a tarefa corretamente.

6. Padrão “time‑budget”: deadline comum para um grupo de operações

Em aplicativos complexos, muitas vezes é necessário definir um timeout comum para executar um grupo de operações. Por exemplo, se o usuário aguarda a resposta por no máximo 2 segundos, e internamente é preciso fazer 3 requisições de rede — todas devem respeitar o mesmo deadline.

Como propagar o deadline para baixo na pilha?

  • Passe um objeto de deadline (por exemplo, Instant deadline) para todos os métodos potencialmente bloqueantes.
  • Em cada método, calcule o tempo restante: Duration.between(Instant.now(), deadline).
  • Use esse tempo para timeouts em operações bloqueantes (await(timeout), poll(timeout), orTimeout(timeout)).
Instant deadline = Instant.now().plusSeconds(2);

void doWork(Instant deadline) throws TimeoutException, InterruptedException {
    Duration left = Duration.between(Instant.now(), deadline);
    if (left.isNegative() || left.isZero()) throw new TimeoutException();
    // Usamos left para o timeout
    queue.poll(left.toMillis(), TimeUnit.MILLISECONDS);
}

Scoped Values / contexto

No Java 21+ é possível usar Scoped Values para passar o deadline pela pilha de chamadas, sem ter que passá‑lo explicitamente para cada método.

7. Structured Concurrency: cancelar todo o scope em caso de falha/timeout

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> f1 = scope.fork(() -> fetchData1());
    Future<String> f2 = scope.fork(() -> fetchData2());

    boolean completed = scope.joinUntil(Instant.now().plusSeconds(2));
    if (!completed) {
        scope.shutdown();
        throw new TimeoutException("O deadline expirou!");
    }
    scope.throwIfFailed();
    // ...
}
  • Se o deadline expirar — o scope cancela todas as tarefas.
  • Se uma tarefa falhar — as demais são canceladas automaticamente.

8. Erros comuns ao lidar com cancelamento e timeouts

Erro nº 1: Esperar que interrupt() termine a thread instantaneamente. Na verdade, é apenas um sinal — a tarefa deve verificar o status e encerrar corretamente.

Erro nº 2: Não verificar isInterrupted() em laços longos. Se não verificar o sinalizador de interrupção, a tarefa continuará executando para sempre, mesmo que tenha sido solicitada a finalizar.

Erro nº 3: Future.cancel() não leva ao cancelamento se a tarefa não reage a interrupt. Se a tarefa for “surda”, cancel() não ajudará.

Erro nº 4: Não propagar timeouts para baixo na pilha. Se você não passar o deadline para todos os métodos, uma operação interna pode “travar” por mais tempo do que o necessário.

Erro nº 5: Em cadeias de thenCompose de CompletableFuture o cancelamento não é propagado automaticamente. Se cancelar o future “de cima”, tarefas internas podem continuar executando — trate o cancelamento explicitamente.

Erro nº 6: StructuredTaskScope não é fechado (sem try‑with‑resources). Se o scope não for fechado, tarefas filhas podem ficar “penduradas”.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION