从List <Future>到Future <List>序列


75

我正在尝试转换List<CompletableFuture<X>>CompletableFuture<List<T>>。当您有许多异步任务并且需要获得所有异步任务的结果时,这非常有用。

如果它们中的任何一个失败,那么最终的未来将失败。这就是我实现的方式:

  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));
    }

要运行它:

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);

如果其中任何一个失败,则失败。即使有一百万个期货,它也能提供预期的输出。我的问题是:假设如果有超过5000个期货,并且其中任何一个失败,我都会得到StackOverflowError

java.util.concurrent.CompletableFuture.internalComplete(CompletableFuture.java:210)处的线程“ pool-1-thread-2611”中的java.lang.StackOverflowError,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)

我做错了什么?

注意:当任何将来失败时,上述返回的将来都会失败。接受的答案也应考虑这一点。


1
如果我是你,我会Collector
改成

@fge这实际上是一个很好的建议。我来自scala世界,那里有类似的事情。收藏家可能更适合这里。但是我想的实现可能是相似的。
贾廷,2015年

Answers:


93

用途CompletableFuture.allOf(...)

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

关于您的实现的一些评论:

您使用的.thenComposeAsync.thenApplyAsync并且.thenCombineAsync很可能没有做你的期望。这些...Async方法在单独的线程中运行提供给它们的函数。因此,在您的情况下,您导致将新项添加到列表中以在提供的执行程序中运行。无需将轻量级操作填充到缓存的线程执行器中。请勿在没有thenXXXXAsync充分理由的情况下使用方法。

另外,reduce不应用于堆积到易变容器中。即使在流是顺序的流时它可能正确工作,但是如果将流设为并行流,它将失败。要执行可变减少,请.collect改用。

如果要在第一次失败后立即异常完成整个计算,请在您的sequence方法中执行以下操作:

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

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

return result;

此外,如果您想在第一次失败时取消其余操作,请在exec.shutdownNow();之后添加result.completeExceptionally(ex);。当然,这假定exec仅针对这一计算存在。如果没有,则必须循环遍历并分别取消剩余的Future每个。


1
我不明白的一件事是,allof返回类型为CompletableFuture<Void>,我们返回时CompletableFuture<List<T>>没有任何编译器警告。我不知道虚无的本质
贾廷,2015年

1
@Jatin我想你可能是对的。早上醒来时,我会重新考虑它,并相应地修改答案。
米沙(Misha)2015年

1
@Jatin是的,在当前的实现中reduce,只要方法中的流sequence2保持顺序,ArrayList是安全的。但是,如果并行创建流,则编写中断的流结构是非常不希望的。至少,如果您依赖于流是连续的,则第三个参数reduce应为(a, b) -> {throw new IllegalStateException("Parallel not allowed");}
Misha

1
这就是您原始解决方案(使用thenCombine)的行为方式。如果您想缩短计算并立即触发异常完成,这很容易做到。查看最新答案。
米沙(Misha)2015年

1
@AbhijitSarkar任务未由调用join。使用的好处allOf是,在allOf触发时,所有任务都已完成并且join仅获得结果。
Misha

11

正如Misha指出的那样,您正在过度使用…Async操作。此外,您将组成一个复杂的操作链,对不依赖您程序逻辑的依赖关系进行建模:

  • 您创建一个作业x,该作业取决于列表中的第一和第二作业
  • 您创建一个作业x + 1,该作业取决于作业x和列表中的第三个作业
  • 您创建一个作业x + 2,该作业取决于作业x + 1和列表中的第4个作业
  • 您创建一个作业x + 5000,具体取决于作业x + 4999和列表中的最后一个作业

然后,取消(显式或由于异常)此递归组合的作业可能会递归执行,并可能失败,并显示StackOverflowError。那取决于实现。

Misha所示,有一种方法,allOf可以让您建模自己的初衷,以定义一个依赖于列表中所有作业的作业。

但是,值得注意的是,即使那不是必需的。由于您使用的是无限制线程池执行器,因此您只需发布一个异步作业,将结果收集到列表中就可以完成。无论如何,通过询问每个作业的结果来暗示等待完成。

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);

当线程数量受到限制并且作业可能会产生其他异步作业时,使用方法来构成相关操作很重要,以避免避免等待的作业从必须先完成的作业中窃取线程,但在此情况并非如此。

在这种特定情况下,一个作业简单地遍历大量的先决条件作业,并在必要时等待,可能比对大量依赖项建模并使每个作业将完成情况通知依赖项作业更为有效。


2
一个警告是,使用supplyAsync而不是allOf将消耗池中的线程以等待所有任务的完成。如果我没记错的话,allOf将在分配给各个任务的线程内运行。对于大多数用例来说,这并不是什么大问题,但是值得注意。
Misha 2015年

1
@Misha:我确实提到过,如果线程数量有限,它将窃取线程,并且由于使用了无限线程池执行程序(并且没有生成异步子作业),所以它可以在这里工作。
Holger

@Holger回答的一个问题是:如果以后的任何将来失败,它仍将等待加入的一个完成。相反,一旦发生故障,返回的未来应立即失效。
贾汀,2015年

其实,我对这个事实还算不错。但不是线程窃取。
贾廷,2015年

9

您可以获取Spotify的CompletableFutures库和使用allAsList方法。我认为这是受番石榴Futures.allAsList方法启发的。

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

如果您不想使用库,这是一个简单的实现:

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())
    );
}

5

要添加@Misha接受的答案,可以将其进一步扩展为收集器:

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

现在你可以:

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

3

在CompletableFuture上使用thenCombine的示例序列操作

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);  

   }
} 

如果您不介意使用第三方库,cyclops-react(我是作者)为CompletableFutures(以及Optionals,Streams等)提供了一组实用程序方法

  List<CompletableFuture<String>> listOfFutures;

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

1

免责声明:这不会完全回答最初的问题。它将缺少“如果一个失败就全部失败”部分。但是,我无法回答实际的,更笼统的问题,因为它是与此重复的问题:Java 8 CompletableFuture.allOf(...),带有Collection或List。所以我会在这里回答:

如何转换List<CompletableFuture<V>>CompletableFuture<List<V>>使用Java 8的流API?

摘要:使用以下内容:

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);
}

用法示例:

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

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

完整的例子:

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);
    }

}

您应该使用thenCombine()而不是thenApply()在累加器中,以避免join()调用。否则,调用线程将实际执行该操作,因此该集合仅在所有操作完成后才返回。您可以通过在futureList.join():之前添加打印来进行检查:只有在所有期货都打印了“成功加载的测试数据”之后,才打印该打印。
Didier L

@DidierL如果更改thenApply()为,thenCombine()则对的最后一次join()调用CompletableFuture<List<V>>将不再阻塞,而是立即返回空结果。因此,列表的未来将不会等到所有单独的未来都完成之后。但这是整个事情的最初想法。
Kai Stapel

是的,的确,我忘记了Collector依赖突变。您的代码的问题在于它等同于CompletableFuture.completedFuture(listOfFutures.stream().map(CompletableFuture::join).collect(toList()));。该集合实际上返回的是已经完成的将来,因此不再需要返回将来。
Didier L

您可能是正确的,它在功能上等同于我的“完整示例”。但是,该示例仅用于说明有关如何使用toFutureList()收集器的目的。不等价的是listOfFutures.stream().map(CompletableFuture::join).collect(toList())listOfFutures.stream().collect(toFutureList())。前者为您提供了一个完整的结果,而所有期货都已完成,而后者则为您提供了一个可以传递的值列表或映射到其他值而不会阻塞的未来。
Kai Stapel

那就是你错了:后者做的完全一样。您的收集器仅join()在调用线程上调用所有期货,然后将结果包装在一个completed中CompletableFuture它正在阻塞。正如我之前说的,只需在流收集之后添加一个打印件,您就会看到该打印件仅在所有期货完成后才出现。
Didier L

0

除了Spotify Futures库之外,您还可以尝试在以下位置找到我的代码:https : //github.com/vsilaev/java-async-await/blob/master/net.tascalate.async.examples/src/main/java/net/ tascalate / concurrent / CompletionStages.java(具有对同一包中其他类的依赖)

它实现了一种逻辑,该逻辑返回“ M个中至少N个” CompletionStage-s,并带有一个策略,允许其容忍多少错误。有适用于所有情况的便捷方法,还有针对剩余期货的取消政策,以及与CompletionStage-s(接口)而非CompletableFuture(具体类)相关的代码。


更新:建议的代码移到单独的库中,github.com
vsilaev/

如果链接停止工作,此答案将无用。请在答案中嵌入代码。
西蒙·佛斯伯格

0

Javaslang有一个非常方便的FutureAPI。它还允许从期货集合中创建期货集合。

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

参见http://static.javadoc.io/io.javaslang/javaslang/2.0.5/javaslang/concurrent/Future.html#sequence-java.lang.Iterable-


2
我喜欢答案。但这取决于javaslang.concurrent.Future:(
Jatin

没错-但是使用过Javaslang Future后,您不想回到Java Future或CompletableFuture
Mathias Dpunkt

0

您的任务可以轻松完成,例如,

final List<CompletableFuture<Module> futures =...
CompletableFuture.allOf(futures.stream().toArray(CompletableFuture[]::new)).join();
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.