【问题标题】:CompletableFuture#whenComplete not called if thenApply is usedCompletableFuture#whenComplete 如果使用 thenApply 则不调用
【发布时间】:2017-03-08 13:49:12
【问题描述】:

我有以下代码(来自my previous question)在远程服务器上安排任务,然后使用ScheduledExecutorService#scheduleAtFixedRate 轮询完成。任务完成后,它会下载结果。 我想将Future 返回给调用者,以便他们可以决定何时阻止以及阻止多长时间,并为他们提供取消任务的选项。

我的问题是,如果客户端取消download 方法返回的FuturewhenComplete 块不会执行。如果我删除 thenApply 它确实如此。很明显,我对 Future 作文有一些误解……我应该改变什么?

public Future<Object> download(Something something) {
    String jobId = schedule(something);
    CompletableFuture<String> job = pollForCompletion(jobId);
    return job.thenApply(this::downloadResult);
}

private CompletableFuture<String> pollForCompletion(String jobId) {
    ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    CompletableFuture<String> completionFuture = new CompletableFuture<>();

    ScheduledFuture<?> checkFuture = executor.scheduleAtFixedRate(() -> {           
            if (pollRemoteServer(jobId).equals("COMPLETE")) {
                completionFuture.complete(jobId);
            }
    }, 0, 10, TimeUnit.SECONDS);
    completionFuture                
            .whenComplete((result, thrown) -> {
                System.out.println("XXXXXXXXXXX"); //Never happens unless thenApply is removed
                checkFuture.cancel(true);
                executor.shutdown();
            });
    return completionFuture;
}

同样,如果我这样做:

return completionFuture.whenComplete(...)

而不是

completionFuture.whenComplete(...);
return completionFuture;

whenComplete 也永远不会执行。这对我来说似乎非常违反直觉。从逻辑上讲,whenComplete 返回的Future 不应该是我应该坚持的那个吗?

编辑:

我更改了代码以明确反向传播取消。它令人憎恶且难以阅读,但它确实有效,我找不到更好的方法:

public Future<Object> download(Something something) throws ChartDataGenException, Exception {
        String jobId = schedule(something);
        CompletableFuture<String> job = pollForCompletion(jobId);
        CompletableFuture<Object> resulting = job.thenApply(this::download);
        resulting.whenComplete((result, thrown) -> {
            if (resulting.isCancelled()) { //the check is not necessary, but communicates the intent better
                job.cancel(true);
            }
        });
        return resulting;
}

编辑 2:

我发现了tascalate-concurrent,这是一个出色的库,提供了CompletionStage 的合理实现,支持可以透明地反向传播取消的依赖承诺(通过DependentPromise 类)。似乎非常适合这个用例。

这就够了:

DependentPromise
          .from(pollForCompletion(jobId))
          .thenApply(this::download, true); //true means the cancellation should back-propagate

请注意,没有测试过这种方法。

【问题讨论】:

  • 这是我的新理解: 1.在申请whenComplete之前通过这个阶段是正确的,因为你必须取消这个阶段才能让它触发下游的动作whenComplete 舞台。如果您取消whenComplete 阶段,它根本不会执行。 2.您还必须坚持whenComplete阶段以确保其可达性。令人困惑...
  • 我已尝试根据您的代码重现您的问题(添加缺少的部分),但我没有遇到您的问题:whenComplete() 始终执行(在计划任务完成以后) ,并且客户端取消绝对没有效果——取消不会反向传播。此外,如果我实现取消的反向传播,当客户端取消未来时,whenComplete() 仍会执行。你能把这段代码变成minimal reproducible example吗?
  • @Didier L:我想,取消不会反向传播的事实正是 OP 必须实现的。
  • @Holger 可能确实是下一步,但这并不能解释为什么whenComplete 没有被执行
  • 对于反向传播,你也可以测试resulting.isCancelled(),而不是依赖instanceof

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


【解决方案1】:

你的结构如下:

           ┌──────────────────┐
           │ completionFuture |
           └──────────────────┘
             ↓              ↓
  ┌──────────────┐      ┌───────────┐
  │ whenComplete |      │ thenApply |
  └──────────────┘      └───────────┘

因此,当您取消 thenApply 未来时,原始 completionFuture 对象不受影响,因为它不依赖于 thenApply 阶段。但是,如果您不链接 thenApply 阶段,那么您将返回原始的 completionFuture 实例,并且取消此阶段会导致取消所有相关阶段,从而导致立即执行 whenComplete 操作。

但是当thenApply 阶段被取消时,completionFuture 仍然可能在满足pollRemoteServer(jobId).equals("COMPLETE") 条件时完成,因为轮询不会停止。但是我们不知道jobId = schedule(something)pollRemoteServer(jobId)的关系。如果您的应用程序状态更改为在取消下载后永远无法满足此条件,则此未来将永远不会完成……


关于你的最后一个问题,哪个未来是“我应该坚持的?”,实际上并不要求有一个线性的未来链,而CompletableFuture的便捷方法可以很容易地创建这样的链通常是最没用的事情,因为如果你有线性依赖,你可以只写一段代码。您链接两个独立阶段的模型是正确的,但取消不能通过它起作用,但它也不能通过线性链起作用。

如果您希望能够取消源阶段,则需要对其进行引用,但如果您希望能够获得依赖阶段的结果,则还需要对该阶段的引用。

【讨论】:

  • OP 并不需要 whenComplete 的结果。但我的理解是这个阶段必须明确保留,否则无法到达,在取消的时候可能存在也可能不存在。
  • @MarkoTopolnik 我猜你调用 whenComplete 的原始未来保留了对链式的引用......否则,它会非常糟糕。
  • @kaqqao 这可能是正确的,因为人们期望它的实现方式,但它仍然是未指定的行为并且依赖于不健康。
  • @kaqqao: 你不需要投票resulting.isCancelled() 因为thrown 将是CancellationException。但实际上,您根本不需要任何检查。您可以简单地使用resulting.whenComplete((result, thrown) -&gt; job.cancel(true));作为函数执行时,resulting已被取消或job已完成,在这种情况下它将忽略取消。
  • @Marko Topolnik:每次我submit 一个工作到一个ExecutorService 或注册一个事件源的侦听器时,我假设它将保留对对象的引用以履行合同。嗯,其实,确实不能保证object还活着,不过没关系,只要执行了关联动作,也就是说,这个是什么是所有关于。我们希望执行BiConsumeraccept 方法; whenComplete 返回的 CompletableFuture 无关紧要……
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多