web-dev-qa-db-fra.com

Comment interrompre l'exécution sous-jacente de CompletableFuture

Je sais que CompletableFuture design ne contrôle pas son exécution avec des interruptions, mais je suppose que certains d’entre vous ont peut-être ce problème. CompletableFutures est un très bon moyen de composer une exécution asynchrone, mais dans le cas où vous voulez que l'exécution sous-jacente soit interrompue ou arrêtée lorsque le futur est annulé, comment pouvons-nous le faire? Ou devons-nous simplement accepter que toute CompletableFuture annulée ou complétée manuellement n’aura pas d’impact sur le fil de travail pour l’achever?

À mon avis, il s’agit évidemment d’un travail inutile qui prend du temps à l’exécuteur testamentaire. Je me demande quelle approche ou quel design pourrait aider dans ce cas?

METTRE À JOUR 

Voici un test simple pour cette

public class SimpleTest {

  @Test
  public void testCompletableFuture() throws Exception {
    CompletableFuture<Void> cf = CompletableFuture.runAsync(()->longOperation());

    bearSleep(1);

    //cf.cancel(true);
    cf.complete(null);

    System.out.println("it should die now already");
    bearSleep(7);
  }

  public static void longOperation(){
    System.out.println("started");
    bearSleep(5);
    System.out.println("completed");
  }

  private static void bearSleep(long seconds){
    try {
      TimeUnit.SECONDS.sleep(seconds);
    } catch (InterruptedException e) {
      System.out.println("OMG!!! Interrupt!!!");
    }
  }
}
21
vach

A CompletableFuture n'est pas lié à l'action asynchrone qui peut éventuellement la terminer.

Puisque (contrairement à FutureTask) cette classe n’a aucun contrôle direct sur le calcul de Qui l’entraîne, l’annulation est traitée comme une Simplement une autre forme d’achèvement exceptionnel. La méthode cancel a Le même effet que completeExceptionally(new CancellationException()).

Il se peut que même pas {be} _ un thread distinct travaille pour le compléter (il peut même y avoir plusieurs threads dessus). Même s'il y en a, il n'y a pas de lien entre une CompletableFuture et un fil qui y fait référence.

En tant que tel, vous ne pouvez rien faire avec CompletableFuture pour interrompre un thread susceptible d'exécuter une tâche qui l'achèvera. Vous devrez écrire votre propre logique qui suit toutes les instances Thread qui acquièrent une référence à CompletableFuture avec l'intention de la compléter.


Voici un exemple du type d'exécution, je pense que vous pourriez vous en sortir.

public static void main(String[] args) throws Exception {
    ExecutorService service = Executors.newFixedThreadPool(1);
    CompletableFuture<String> completable = new CompletableFuture<>();
    Future<?> future = service.submit(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                if (Thread.interrupted()) {
                    return; // remains uncompleted
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    return; // remains uncompleted
                }
            }
            completable.complete("done");
        }
    });

    Thread.sleep(2000);

    // not atomic across the two
    boolean cancelled = future.cancel(true);
    if (cancelled)
        completable.cancel(true); // may not have been cancelled if execution has already completed
    if (completable.isCancelled()) {
        System.out.println("cancelled");
    } else if (completable.isCompletedExceptionally()) {
        System.out.println("exception");
    } else {
        System.out.println("success");
    }
    service.shutdown();
}

Cela suppose que la tâche en cours d'exécution est configurée pour gérer correctement les interruptions.

14

Et ça?

public static <T> CompletableFuture<T> supplyAsync(final Supplier<T> supplier) {

    final ExecutorService executorService = Executors.newFixedThreadPool(1);

    final CompletableFuture<T> cf = new CompletableFuture<T>() {
        @Override
        public boolean complete(T value) {
            if (isDone()) {
                return false;
            }
            executorService.shutdownNow();
            return super.complete(value);
        }

        @Override
        public boolean completeExceptionally(Throwable ex) {
            if (isDone()) {
                return false;
            }
            executorService.shutdownNow();
            return super.completeExceptionally(ex);
        }
    };

    // submit task
    executorService.submit(() -> {
        try {
            cf.complete(supplier.get());
        } catch (Throwable ex) {
            cf.completeExceptionally(ex);
        }
    });

    return cf;
}

Test simple:

    CompletableFuture<String> cf = supplyAsync(() -> {
        try {
            Thread.sleep(1000L);
        } catch (Exception e) {
            System.out.println("got interrupted");
            return "got interrupted";
        }
        System.out.println("normal complete");
        return "normal complete";
    });

    cf.complete("manual complete");
    System.out.println(cf.get());

Je n'aime pas l'idée de devoir créer un service Executor à chaque fois, mais vous pouvez peut-être trouver un moyen de réutiliser ForkJoinPool.

2
Ruben

Veuillez vous reporter à ma réponse à la question connexe: Transformez l’avenir de Java en avenir complet

Dans le code mentionné, le comportement CompletionStage est ajouté à la sous-classe RunnableFuture (utilisée par les implémentations ExecutorService), afin que vous puissiez l'interrompre correctement.

1
Valery Silaev

Qu'en est-il de?

/** @return {@link CompletableFuture} which when cancelled will interrupt the supplier
 */
public static <T> CompletableFuture<T> supplyAsyncInterruptibly(Supplier<T> supplier, Executor executor) {
    return produceInterruptibleCompletableFuture((s) -> CompletableFuture.supplyAsync(s, executor), supplier);
}

// in case we want to do the same for similar methods later
private static <T> CompletableFuture<T> produceInterruptibleCompletableFuture(
        Function<Supplier<T>,CompletableFuture<T>> completableFutureAsyncSupplier, Supplier<T> action) {
    FutureTask<T> task = new FutureTask<>(action::get);
    return addCancellationAction(completableFutureAsyncSupplier.apply(asSupplier(task)), () ->
            task.cancel(true));
}

/** Ensures the specified action is executed if the given {@link CompletableFuture} is cancelled.
 */
public static <T> CompletableFuture<T> addCancellationAction(CompletableFuture<T> completableFuture,
                                                             @NonNull Runnable onCancellationAction) {
    completableFuture.whenComplete((result, throwable) -> {
        if (completableFuture.isCancelled()) {
            onCancellationAction.run();
        }
    });
    return completableFuture;  // return original CompletableFuture
}

/** @return {@link Supplier} wrapper for the given {@link RunnableFuture} which calls {@link RunnableFuture#run()}
 *          followed by {@link RunnableFuture#get()}.
 */
public static <T> Supplier<T> asSupplier(RunnableFuture<T> futureTask) throws CompletionException {
    return () -> {
        try {
            futureTask.run();
            try {
                return futureTask.get();
            } catch (ExecutionException e) {  // unwrap ExecutionExceptions
                final Throwable cause = e.getCause();
                throw (cause != null) ? cause : e;
            }
        } catch (CompletionException e) {
            throw e;
        } catch (Throwable t) {
            throw new CompletionException(t);
        }
    };
}
0
user6519354