web-dev-qa-db-fra.com

Lister la séquence <Future> à la future <List>

J'essaie de convertir List<CompletableFuture<X>> en CompletableFuture<List<T>>. Ceci est très utile lorsque vous avez plusieurs tâches asynchrones et que vous devez obtenir des résultats pour toutes. 

Si l'un d'entre eux échoue, l'avenir final échoue. Voici comment j'ai implémenté:

  public static <T> CompletableFuture<List<T>> sequence2(List<CompletableFuture<T>> com, ExecutorService exec) {
        if(com.isEmpty()){
            throw new IllegalArgumentException();
        }
        Stream<? extends CompletableFuture<T>> stream = com.stream();
        CompletableFuture<List<T>> init = CompletableFuture.completedFuture(new ArrayList<T>());
        return stream.reduce(init, (ls, fut) -> ls.thenComposeAsync(x -> fut.thenApplyAsync(y -> {
            x.add(y);
            return x;
        },exec),exec), (a, b) -> a.thenCombineAsync(b,(ls1,ls2)-> {
            ls1.addAll(ls2);
            return ls1;
        },exec));
    }

Pour l'exécuter:

ExecutorService executorService = Executors.newCachedThreadPool();
        Stream<CompletableFuture<Integer>> que = IntStream.range(0,100000).boxed().map(x -> CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep((long) (Math.random() * 10));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return x;
        }, executorService));
CompletableFuture<List<Integer>> sequence = sequence2(que.collect(Collectors.toList()), executorService);

Si l'un d'entre eux échoue, il échoue. Il donne la production attendue même s’il ya un million de contrats à terme. Le problème que j'ai est le suivant: Dites s'il y a plus de 5000 futures et si l'un d'entre eux échoue, je reçois un StackOverflowError:

Exception dans le thread "pool-1-thread-2611" Java.lang.StackOverflowError à Java.util.concurrent.CompletableFuture.internalComplete (CompletableFuture.Java:210) à Java.util.concurrent.CompletableFuture $ ThenCompose.run (CompletableFuture.Java:1487) à Java.util.concurrent.CompletableFuture.postComplete (CompletableFuture.Java:193) à Java.util.concurrent.CompletableFuture.internalComplete (CompletableFuture.Java:210) à Java.util.concurrent.CompletableFuture $ ThenCompose.run (CompletableFuture.Java:1487)

Qu'est-ce que je fais mal?

Remarque: le futur renvoyé ci-dessus échoue correctement en cas d'échec d'un des futurs. La réponse acceptée devrait également prendre ce point.

55
Jatin

Utilisez CompletableFuture.allOf(...):

static<T> CompletableFuture<List<T>> sequence(List<CompletableFuture<T>> com) {
    return CompletableFuture.allOf(com.toArray(new CompletableFuture<?>[com.size()]))
            .thenApply(v -> com.stream()
                .map(CompletableFuture::join)
                .collect(toList())
            );
}

Quelques commentaires sur votre implémentation:

Votre utilisation de .thenComposeAsync, .thenApplyAsync et .thenCombineAsync ne répond probablement pas à vos attentes. Ces méthodes ...Async exécutent la fonction qui leur est fournie dans un thread séparé. Ainsi, dans votre cas, vous faites en sorte que le nouvel élément de la liste soit exécuté dans l'exécuteur fourni. Il n'est pas nécessaire de rentrer des opérations légères dans un exécuteur de thread en cache. N'utilisez pas les méthodes thenXXXXAsync sans une bonne raison. 

De plus, reduce ne doit pas être utilisé pour s'accumuler dans des conteneurs mutables. Même si cela peut fonctionner correctement lorsque le flux est séquentiel, il échouera si le flux doit être rendu parallèle. Pour effectuer une réduction mutable, utilisez plutôt .collect.

Si vous souhaitez effectuer l'intégralité du calcul exceptionnellement immédiatement après le premier échec, procédez comme suit dans votre méthode sequence:

CompletableFuture<List<T>> result = CompletableFuture.allOf(com.toArray(new CompletableFuture<?>[com.size()]))
        .thenApply(v -> com.stream()
                .map(CompletableFuture::join)
                .collect(toList())
        );

com.forEach(f -> f.whenComplete((t, ex) -> {
    if (ex != null) {
        result.completeExceptionally(ex);
    }
}));

return result;

Si, en outre, vous souhaitez annuler les opérations restantes lors du premier échec, ajoutez exec.shutdownNow(); immédiatement après result.completeExceptionally(ex);. Ceci, bien sûr, suppose que exec n'existe que pour ce calcul. Si ce n'est pas le cas, vous devrez boucler et annuler chaque Future restante individuellement.

71
Misha

Comme Misha a fait remarquer , vous utilisez trop d'opérations …Async. De plus, vous composez une chaîne complexe d’opérations modélisant une dépendance qui ne reflète pas la logique de votre programme:

  • vous créez un emploi x qui dépend des premier et deuxième emplois de votre liste
  • vous créez un emploi x + 1 qui dépend de l'emploi x et du troisième emploi de votre liste
  • vous créez un travail x + 2 qui dépend du travail x + 1 et du 4ème emploi de votre liste
  • vous créez un travail x + 5000 qui dépend du travail x + 4999 et du dernier emploi de votre liste

Ensuite, l'annulation (explicitement ou en raison d'une exception) de ce travail composé de manière récursive peut être effectuée de manière récursive et échouer avec une variable StackOverflowError. Cela dépend de la mise en œuvre.

Comme déjà montré par Misha , il existe une méthode, allOf qui vous permet de modéliser votre intention initiale, de définir un travail qui dépend de tous les travaux de votre liste.

Toutefois, il convient de noter que même cela n’est pas nécessaire. Puisque vous utilisez un exécuteur de pool de threads sans limite, vous pouvez simplement poster un travail asynchrone rassemblant les résultats dans une liste et vous avez terminé. Attendre l'achèvement est implicite en demandant quand même le résultat de chaque travail.

ExecutorService executorService = Executors.newCachedThreadPool();
List<CompletableFuture<Integer>> que = IntStream.range(0, 100000)
  .mapToObj(x -> CompletableFuture.supplyAsync(() -> {
    LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos((long)(Math.random()*10)));
    return x;
}, executorService)).collect(Collectors.toList());
CompletableFuture<List<Integer>> sequence = CompletableFuture.supplyAsync(
    () -> que.stream().map(CompletableFuture::join).collect(Collectors.toList()),
    executorService);

Il est important d’utiliser des méthodes pour composer des opérations dépendantes, lorsque le nombre de threads est limité et que les travaux peuvent générer d’autres travaux asynchrones, afin d’éviter que des travaux en attente volent des threads à des travaux devant s’achever en premier, mais ce n’est pas le cas ici.

Dans ce cas spécifique, une tâche simplement effectuer une itération sur ce grand nombre de tâches prérequises et attendre si nécessaire peut s'avérer plus efficace que de modéliser ce nombre élevé de dépendances et que chaque tâche informe le travail dépendant de son achèvement.

10
Holger

Vous pouvez obtenir la bibliothèque CompletableFutures de Spotify et utiliser allAsList method. Je pense que cela s’inspire de la méthode Futures.allAsList de Guava.

public static <T> CompletableFuture<List<T>> allAsList(
    List<? extends CompletionStage<? extends T>> stages) {

Et voici une implémentation simple si vous ne souhaitez pas utiliser une bibliothèque:

public <T> CompletableFuture<List<T>> allAsList(final List<CompletableFuture<T>> futures) {
    return CompletableFuture.allOf(
        futures.toArray(new CompletableFuture[futures.size()])
    ).thenApply(ignored ->
        futures.stream().map(CompletableFuture::join).collect(Collectors.toList())
    );
}
6
oskansavli

Pour ajouter la réponse acceptée par @Misha, celle-ci peut être étendue en tant que collecteur:

 public static <T> Collector<CompletableFuture<T>, ?, CompletableFuture<List<T>>> sequenceCollector() {
    return Collectors.collectingAndThen(Collectors.toList(), com -> sequence(com));
}

Maintenant vous pouvez:

Stream<CompletableFuture<Integer>> stream = Stream.of(
    CompletableFuture.completedFuture(1),
    CompletableFuture.completedFuture(2),
    CompletableFuture.completedFuture(3)
);
CompletableFuture<List<Integer>> ans = stream.collect(sequenceCollector());
4
Jatin

Javaslang a une API très pratique Future . Cela permet également de créer un avenir de collecte à partir d’une collection d’avenir.

List<Future<String>> listOfFutures = ... 
Future<Seq<String>> futureOfList = Future.sequence(listOfFutures);

Voir http://static.javadoc.io/io.javaslang/javaslang/2.0.5/javaslang/concurrent/Future.html#sequence-Java.lang.Iterable-

1
Mathias Dpunkt

Un exemple d'opération de séquence utilisant thenCombine sur CompletableFuture

public<T> CompletableFuture<List<T>> sequence(List<CompletableFuture<T>> com){

    CompletableFuture<List<T>> identity = CompletableFuture.completedFuture(new ArrayList<T>());

    BiFunction<CompletableFuture<List<T>>,CompletableFuture<T>,CompletableFuture<List<T>>> combineToList = 
            (acc,next) -> acc.thenCombine(next,(a,b) -> { a.add(b); return a;});

    BinaryOperator<CompletableFuture<List<T>>> combineLists = (a,b)-> a.thenCombine(b,(l1,l2)-> { l1.addAll(l2); return l1;}) ;  

    return com.stream()
              .reduce(identity,
                      combineToList,
                      combineLists);  

   }
} 

Si cela ne vous dérange pas d'utiliser des bibliothèques tierces cyclops-react (je suis l'auteur) dispose d'un ensemble de méthodes utilitaires pour CompletableFutures (et Optionals, Streams, etc.)

  List<CompletableFuture<String>> listOfFutures;

  CompletableFuture<ListX<String>> sequence =CompletableFutures.sequence(listOfFutures);
1
John McClean

En plus de la bibliothèque Spotify Futures, vous pouvez essayer mon code à l'adresse suivante: https://github.com/vsilaev/Java-async-await/blob/master/net.tascalate.async.examples/src/main/Java/ net/tascalate/concurrent/CompletionStages.Java (a ​​des dépendances avec d'autres classes du même package)

Il implémente une logique pour renvoyer "au moins N sur M" CompletionStage-s avec une stratégie indiquant le nombre d'erreurs qu'il est autorisé à tolérer. Il existe des méthodes pratiques pour tous/tous les cas, plus une politique d'annulation pour les contrats à terme restants, plus le code traite de CompletionStage-s (interface) plutôt que de CompletableFuture (classe concrète).

1
Valery Silaev

Avertissement: Cela ne répondra pas complètement à la question initiale. Il manquera la partie "échouer tous si on échoue". Cependant, je ne peux pas répondre à la question plus générique, car elle a été clôturée comme une copie de celle-ci: Java 8 CompletableFuture.allOf (...) avec Collection ou List . Je vais donc répondre ici:

Comment convertir List<CompletableFuture<V>> en CompletableFuture<List<V>> utilisant l'API de flux de Java 8?

Résumé: Utilisez ce qui suit:

private <V> CompletableFuture<List<V>> sequence(List<CompletableFuture<V>> listOfFutures) {
    CompletableFuture<List<V>> identity = CompletableFuture.completedFuture(new ArrayList<>());

    BiFunction<CompletableFuture<List<V>>, CompletableFuture<V>, CompletableFuture<List<V>>> accumulator = (futureList, futureValue) ->
        futureValue.thenCombine(futureList, (value, list) -> {
                List<V> newList = new ArrayList<>(list.size() + 1);
                newList.addAll(list);
                newList.add(value);
                return newList;
            });

    BinaryOperator<CompletableFuture<List<V>>> combiner = (futureList1, futureList2) -> futureList1.thenCombine(futureList2, (list1, list2) -> {
        List<V> newList = new ArrayList<>(list1.size() + list2.size());
        newList.addAll(list1);
        newList.addAll(list2);
        return newList;
    });

    return listOfFutures.stream().reduce(identity, accumulator, combiner);
}

Exemple d'utilisation:

List<CompletableFuture<String>> listOfFutures = IntStream.range(0, numThreads)
    .mapToObj(i -> loadData(i, executor)).collect(toList());

CompletableFuture<List<String>> futureList = sequence(listOfFutures);

Exemple complet:

import Java.util.ArrayList;
import Java.util.List;
import Java.util.concurrent.CompletableFuture;
import Java.util.concurrent.Executor;
import Java.util.concurrent.ExecutorService;
import Java.util.concurrent.Executors;
import Java.util.concurrent.ThreadLocalRandom;
import Java.util.function.BiFunction;
import Java.util.function.BinaryOperator;
import Java.util.stream.IntStream;

import static Java.util.stream.Collectors.toList;

public class ListOfFuturesToFutureOfList {

    public static void main(String[] args) {
        ListOfFuturesToFutureOfList test = new ListOfFuturesToFutureOfList();
        test.load(10);
    }

    public void load(int numThreads) {
        final ExecutorService executor = Executors.newFixedThreadPool(numThreads);

        List<CompletableFuture<String>> listOfFutures = IntStream.range(0, numThreads)
            .mapToObj(i -> loadData(i, executor)).collect(toList());

        CompletableFuture<List<String>> futureList = sequence(listOfFutures);

        System.out.println("Future complete before blocking? " + futureList.isDone());

        // this will block until all futures are completed
        List<String> data = futureList.join();
        System.out.println("Loaded data: " + data);

        System.out.println("Future complete after blocking? " + futureList.isDone());

        executor.shutdown();
    }

    public CompletableFuture<String> loadData(int dataPoint, Executor executor) {
        return CompletableFuture.supplyAsync(() -> {
            ThreadLocalRandom rnd = ThreadLocalRandom.current();

            System.out.println("Starting to load test data " + dataPoint);

            try {
                Thread.sleep(500 + rnd.nextInt(1500));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("Successfully loaded test data " + dataPoint);

            return "data " + dataPoint;
        }, executor);
    }

    private <V> CompletableFuture<List<V>> sequence(List<CompletableFuture<V>> listOfFutures) {
        CompletableFuture<List<V>> identity = CompletableFuture.completedFuture(new ArrayList<>());

        BiFunction<CompletableFuture<List<V>>, CompletableFuture<V>, CompletableFuture<List<V>>> accumulator = (futureList, futureValue) ->
            futureValue.thenCombine(futureList, (value, list) -> {
                    List<V> newList = new ArrayList<>(list.size() + 1);
                    newList.addAll(list);
                    newList.add(value);
                    return newList;
                });

        BinaryOperator<CompletableFuture<List<V>>> combiner = (futureList1, futureList2) -> futureList1.thenCombine(futureList2, (list1, list2) -> {
            List<V> newList = new ArrayList<>(list1.size() + list2.size());
            newList.addAll(list1);
            newList.addAll(list2);
            return newList;
        });

        return listOfFutures.stream().reduce(identity, accumulator, combiner);
    }

}
0
Kai Stapel