【问题标题】:CompletableFuture in loop: How to collect all responses and handle errorsCompletableFuture in loop:如何收集所有响应并处理错误
【发布时间】:2018-12-10 21:03:22
【问题描述】:

我正在尝试循环调用 PUT 请求的 rest api。每个电话都是一个CompletableFuture。每个 api 调用都返回一个 RoomTypes.RoomType 类型的对象

  • 我想收集响应(成功和错误) 响应)在不同的列表中。我该如何做到这一点?我确定我 不能使用allOf,因为如果有的话,它不会得到所有的结果 一次调用更新失败。

  • 如何记录每次调用的错误/异常?


public void sendRequestsAsync(Map<Integer, List> map1) {
    List<CompletableFuture<Void>> completableFutures = new ArrayList<>(); //List to hold all the completable futures
    List<RoomTypes.RoomType> responses = new ArrayList<>(); //List for responses
    ExecutorService yourOwnExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    for (Map.Entry<Integer, List> entry :map1.entrySet()) { 
        CompletableFuture requestCompletableFuture = CompletableFuture
                .supplyAsync(
                        () -> 
            //API call which returns object of type RoomTypes.RoomType
            updateService.updateRoom(51,33,759,entry.getKey(),
                           new RoomTypes.RoomType(entry.getKey(),map2.get(entry.getKey()),
                                    entry.getValue())),
                    yourOwnExecutor
            )//Supply the task you wanna run, in your case http request
            .thenApply(responses::add);

    completableFutures.add(requestCompletableFuture);
}

【问题讨论】:

  • 首先,不要在像ArrayList 这样的非线程安全集合上使用thenApply(responses::add),因为它可能会破坏集合结构。此外,allOf 实际上等待所有成功/失败,但文档在这一点上可能不是很明确(我自己实际测试过)。
  • @DidierL “等待”是什么意思?我刚刚尝试过,我可以看到,只要集合中的一个可完成 Future 产生异常,allOf 返回的可完成 Future 就会调用下一个阶段方法(例如 handle)。
  • @EdwinDalorzo 在我的测试中并非如此:它等到最后一个未来完成。也许这取决于你做什么,但这会令人惊讶。
  • @DidierL 当你说“等待”时,你的意思是在所有未来成功或不成功解决之前不会调用可计算未来的下一个阶段方法?跨度>
  • @EdwinDalorzo 确实,是的:allOf 阶段只有在所有期货都完成后才能完成,即使有些失败的冷杉 - AFAICT。

标签: java multithreading asynchronous java-8 completable-future


【解决方案1】:

您可以简单地使用allOf() 来获得一个在您完成所有初始期货时完成的未来(无论是否例外),然后使用Collectors.partitioningBy() 将它们分为成功和失败:

List<CompletableFuture<RoomTypes.RoomType>> completableFutures = new ArrayList<>(); //List to hold all the completable futures
ExecutorService yourOwnExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

for (Map.Entry<Integer, List> entry : map1.entrySet()) {
    CompletableFuture<RoomTypes.RoomType> requestCompletableFuture = CompletableFuture
            .supplyAsync(
                    () ->
                //API call which returns object of type RoomTypes.RoomType
                updateService.updateRoom(51, 33, 759, entry.getKey(),
                        new RoomTypes.RoomType(entry.getKey(), map2.get(entry.getKey()),
                                entry.getValue())),
                    yourOwnExecutor
            );

    completableFutures.add(requestCompletableFuture);
}

CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0]))
        // avoid throwing an exception in the join() call
        .exceptionally(ex -> null)
        .join();
Map<Boolean, List<CompletableFuture<RoomTypes.RoomType>>> result =
        completableFutures.stream()
                .collect(Collectors.partitioningBy(CompletableFuture::isCompletedExceptionally)));

生成的映射将包含一个带有true 的条目用于失败的期货,另一个带有false 键的条目用于成功的期货。然后,您可以检查这 2 个条目以采取相应措施。

请注意,与您的原始代码相比,有 2 个细微的变化:

  • requestCompletableFuture 现在是 CompletableFuture&lt;RoomTypes.RoomType&gt;
  • thenApply(responses::add)responses 列表已删除

关于日志记录/异常处理,只需添加相关的requestCompletableFuture.handle() 以单独记录它们,但保留requestCompletableFuture 而不是handle() 产生的那个。

【讨论】:

  • 我在CompletableFuture::isCompletedExceptionally 得到Non-static method cant be accessed through static context 。但我的方法不是静态的。
  • result 的类型与collect 返回的内容不完全匹配时,我已经看到了。尝试删除对 result 的赋值,然后使用您的 IDE 再次从整个表达式中提取局部变量。
  • 在for循环中使用“entry”不是线程安全的吗?
  • @TokyoMike 可能确实如此,但这是我不想更改的原始问题的代码的一部分。我建议改为在循环中提取键和值,并在 lambda 中捕获它们(而不是条目本身)
【解决方案2】:

或者,也许您可​​以从不同的角度解决问题,而不是强制使用CompletableFuture,而是使用CompletionService

CompletionService 的整体理念是,一旦给定未来的答案准备就绪,它就会被放入队列中,您可以从中消费结果。

替代方案 1:没有 CompletableFuture

CompletionService<String> cs = new ExecutorCompletionService<>(executor);

List<Future<String>> futures = new ArrayList<>();

futures.add(cs.submit(() -> "One"));
futures.add(cs.submit(() -> "Two"));
futures.add(cs.submit(() -> "Three"));
futures.add(cs.submit(() -> { throw new RuntimeException("Sucks to be four"); }));
futures.add(cs.submit(() -> "Five"));


List<String> successes = new ArrayList<>();
List<String> failures = new ArrayList<>();

while (futures.size() > 0) {
    Future<String> f = cs.poll();
    if (f != null) {
        futures.remove(f);
        try {
            //at this point the future is guaranteed to be solved
            //so there won't be any blocking here
            String value = f.get();
            successes.add(value);
        } catch (Exception e) {
            failures.add(e.getMessage());
        }
    }
}

System.out.println(successes); 
System.out.println(failures);

产量:

[One, Two, Three, Five]
[java.lang.RuntimeException: Sucks to be four]

备选方案 2:使用 CompletableFuture

但是,如果您真的需要处理 CompletableFuture,您也可以将它们提交到完成服务,只需将它们直接放入队列即可:

例如,以下变体具有相同的结果:

BlockingQueue<Future<String>> tasks = new ArrayBlockingQueue<>(5);
CompletionService<String> cs = new ExecutorCompletionService<>(executor, tasks);

List<Future<String>> futures = new ArrayList<>();

futures.add(CompletableFuture.supplyAsync(() -> "One"));
futures.add(CompletableFuture.supplyAsync(() -> "Two"));
futures.add(CompletableFuture.supplyAsync(() -> "Three"));
futures.add(CompletableFuture.supplyAsync(() -> { throw new RuntimeException("Sucks to be four"); }));
futures.add(cs.submit(() -> "Five"));

//places all futures in completion service queue
tasks.addAll(futures);

List<String> successes = new ArrayList<>();
List<String> failures = new ArrayList<>();

while (futures.size() > 0) {
    Future<String> f = cs.poll();
    if (f != null) {
        futures.remove(f);
        try {
            //at this point the future is guaranteed to be solved
            //so there won't be any blocking here
            String value = f.get();
            successes.add(value);
        } catch (Exception e) {
            failures.add(e.getMessage());
        }
    }
}

【讨论】:

  • while(futures.size() &gt; 0) 不会在未解决任何未来时变为无限,并且 poll() 将继续扫描已完成的未来(如果有)。你会推荐使用take() 而不是poll()
  • 我认为不用说,在您的问题中,我们正在讨论预期会产生答案或无法产生答案的期货。这需要多长时间在您的问题中并不明显,但可以理解的是,您希望在继续之前获得所有答案。显然,上面的算法将在继续之前收集所有答案。如果您的未来都不是无限循环(这是您的问题中给出的),则 while 循环不会是无限循环。无论您使用poll 还是take 都不会改变最终结果,由您决定。
  • 恐怕这与“替代方案 2”中描述的不同,即没有使用提交,而是直接 addAll,导致轮询返回未完成的期货并随后被阻塞跨度>
  • @PawełPrażak 很有趣。我没有考虑到这一点。稍后我将尝试对其进行审核,看看是否有解决方法,否则我会将其从答案中删除。
  • 我最终做的是删除ExecutorCompletionService,只需使用tasks.poll()if (f.isDone()),如果没有完成,则重新添加到tasks - 不漂亮但它有效;可能使用 peek() 会使 impl 更干净,但我还没看过
【解决方案3】:

对于你想使用 For 循环的地方。这是一个可行的解决方案CompletableFuture.allOf() ->

您想下载一个网站的 100 个不同网页的内容。您可以按顺序执行此操作,但这将花费大量时间。因此,可以编写一个获取网页链接并返回 CompletableFuture 的函数:

CompletableFuture<String> downloadWebPage(String pageLink) {
return CompletableFuture.supplyAsync(() -> {
    // Code to download and return the web page's content
});
} 

循环调用前面的函数,我们使用的是JAVA 8

List<String> webPageLinks = Arrays.asList(...)  // A list of 100 web page links

// Download contents of all the web pages asynchronously
List<CompletableFuture<String>> pageContentFutures = webPageLinks.stream()
    .map(webPageLink -> downloadWebPage(webPageLink))
    .collect(Collectors.toList());


// Create a combined Future using allOf()
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
    pageContentFutures.toArray(new CompletableFuture[pageContentFutures.size()])
);

CompletableFuture.allOf() 的问题在于它返回 CompletableFuture。但是我们可以通过编写几行额外的代码来获得所有包装的 CompletableFutures 的结果

// When all the Futures are completed, call `future.join()` to get their results and collect the results in a list -
CompletableFuture<List<String>> allPageContentsFuture = allFutures.thenApply(v -> {
return pageContentFutures.stream()
       .map(pageContentFuture -> pageContentFuture.join())
       .collect(Collectors.toList());
});

现在让我们计算包含我们关键字的网页数量 ->

// Count the number of web pages having the "CompletableFuture" keyword.
CompletableFuture<Long> countFuture = allPageContentsFuture.thenApply(pageContents -> {
 return pageContents.stream()
        .filter(pageContent -> pageContent.contains("CompletableFuture"))
        .count();
});

System.out.println("Number of Web Pages having CompletableFuture keyword - " + 
    countFuture.get());

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-12-12
    • 1970-01-01
    • 2015-10-07
    • 1970-01-01
    • 1970-01-01
    • 2019-05-08
    • 2017-01-15
    • 1970-01-01
    相关资源
    最近更新 更多