CompletableFuture 的完成处理程序在哪个线程中执行?

Posted

技术标签:

【中文标题】CompletableFuture 的完成处理程序在哪个线程中执行?【英文标题】:In which thread do CompletableFuture's completion handlers execute? 【发布时间】:2018-02-14 01:44:02 【问题描述】:

我对 CompletableFuture 方法有疑问:

public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn)

JavaDoc 是这么说的:

返回一个新的 CompletionStage,当此阶段完成时 通常,以这个阶段的结果作为参数执行 提供的功能。有关规则,请参阅 CompletionStage 文档 涵盖异常完成。

线程呢?这将在哪个线程中执行?如果future由线程池完成呢?

【问题讨论】:

【参考方案1】:

正如@nullpointer 指出的那样,文档会告诉您您需要了解的内容。但是,相关文本出人意料地含糊不清,此处发布的一些 cmets(和答案)似乎依赖于文档不支持的假设。因此,我认为将其分开是值得的。具体来说,我们应该非常仔细地阅读这一段:

为非异步方法的依赖完成提供的操作可以由完成当前 CompletableFuture 的线程执行,也可以由完成方法的任何其他调用者执行。

听起来很简单,但细节很简单。它似乎故意避免描述何时可以在完成线程上调用依赖完成,而不是在调用像thenApply 这样的完成方法期间。正如所写,上面的段落实际上是在乞求我们用假设来填补空白。这很危险,尤其是当主题涉及并发和异步编程时,我们作为程序员开发的许多期望都被推翻了。让我们仔细看看文档没有说什么。

文档确实声称在调用complete() 之前注册的依赖完成将在完成线程上运行。此外,虽然它声明在调用像 thenApply 这样的完成方法时可能调用依赖完成,但它没有声明将调用完成注册它的线程(注意“任何其他”一词)。

对于任何使用CompletableFuture 来安排和编写任务的人来说,这些都是潜在的重要点。考虑一下这一系列事件:

    线程 A 通过 f.thenApply(c1) 注册一个依赖完成。 一段时间后,线程 B 调用 f.complete()。 大约在同一时间,线程 C 通过 f.thenApply(c2) 注册另一个依赖完成。

从概念上讲,complete() 做了两件事:它发布未来的结果,然后尝试调用依赖完成。现在,如果线程 C 在发布结果值之后运行,但线程 B 在调用c1 之前之前会发生什么情况?根据实现,线程 C 可能会看到 f 已完成,然后它可能会调用 c1 c2。或者,线程 C 可以调用 c2,而让线程 B 调用 c1。该文件不排除任何一种可能性。考虑到这一点,以下是文档不支持的假设:

    f 上注册的依赖完成 c在完成之前将在调用 f.complete() 期间被调用; c 将在 f.complete() 返回时运行完成; 将按任何特定顺序(例如,注册顺序)调用依赖完成; 注册的依赖完成之前 f完成将在注册完成之前调用之后 f完成。

考虑另一个例子:

    线程A调用f.complete(); 一段时间后,线程 B 通过f.thenApply(c1) 注册完成; 大约在同一时间,线程 C 通过 f.thenApply(c2) 注册一个单独的完成。

如果已知f 已经运行完成,人们可能会假设c1 将在f.thenApply(c1) 期间被调用,而c2 将在f.thenApply(c2) 期间被调用。人们可能会进一步假设c1 将在f.thenApply(c1) 返回时运行完成。但是,文档支持这些假设。有可能 一个 线程调用 thenApply 最终会调用 both c1c2,而另一个线程不会调用任何一个。

仔细分析 JDK 代码可以确定上述假设场景的结果。但即使这样也是有风险的,因为您最终可能会依赖(1)不可移植或(2)可能更改的实现细节。最好不要假设 javadocs 或原始 JSR 规范中没有说明的任何内容。

tldr:要小心你的假设,并且在编写文档时,要尽可能清晰和深思熟虑。虽然简洁是一件美妙的事情,但要警惕人类填补空白的倾向。

【讨论】:

有趣的分析 - 真正挖掘并发编程领域中实现承诺的复杂性。 似乎在过去,当我阅读该文档时,我应该问自己“完成方法”究竟是什么意思。 “仔细分析 JDK 代码”得出的结论是,您描述的大多数令人惊讶的场景确实是可能的。因此,依赖实施细节的风险相当低。两个独立的动作没有顺序,因此不会按照它们注册的顺序执行,这一事实已经在here 进行了讨论,尽管这甚至不需要你描述的更令人惊讶的场景。 @Holger 我不喜欢他们使用“完成”来描述在完成其先行项后运行的任务的方式。因为这个词在讨论期货时经常出现(“完成”、“运行到完成”等),在 javadoc 摘录之类的上下文中很容易掩盖或误解它。我宁愿他们改用“延续”。 是的,当我第一次阅读时,我认为“完成方法”是指completecompleteExceptionallycancelobtrude…中的任何一个完整的而不是或定义或延续…… @phant0m 不,它不适用于完全不相关的期货。【参考方案2】:

CompletableFuture 文档中指定的政策可以帮助您更好地理解:

为非异步方法的依赖完成提供的操作可能是 由完成当前 CompletableFuture 的线程执行或由完成方法的任何其他调用者

执行所有没有显式 Executor 参数的异步方法 使用ForkJoinPool.commonPool()(除非它不支持 并行度至少为两个,在这种情况下,一个新的线程是 创建以运行每个任务)。为了简化监控、调试和 跟踪,所有生成的异步任务都是marker的实例 接口CompletableFuture.AsynchronousCompletionTask

更新:我还建议阅读@Mike 的this answer,作为对文档细节的进一步有趣分析。

【讨论】:

对于像thenApplythenRun 这样的方法,文档中的内容已经很清楚了。但是allOf呢,对于fa = CompletableFuture.allOf(f0, f1, f2); fa.thenRun(someRunnable),假设f0f1f2分别在线程A、线程B、线程C中完成,那么someRunnable会在哪个线程中执行呢?同样,如果f0.thenCompose(x -&gt; someNewCompletionStageProducer).thenRun(someRunnable)someRunnable 将在f0 的线程或fn 返回的未来中执行,那么thenCompose(Function&lt;? super T,? extends CompletionStage&lt;U&gt;&gt; fn) 呢? @纳曼【参考方案3】:

来自Javadoc:

为非异步方法的依赖完成提供的操作可以由完成当前 CompletableFuture 的线程执行,也可以由完成方法的任何其他调用者执行。

更具体地说:

fn 将在调用complete() 期间在调用complete() 的任何线程的上下文中运行。

如果在调用thenApply()complete() 已经完成,则fn 将在调用thenApply() 的线程上下文中运行。

【讨论】:

【参考方案4】:

在线程方面缺少 API 文档。理解线程和期货是如何工作的需要一些推理。从一个假设开始:CompletableFuture 的非Async 方法不会自行生成新线程。工作将在现有线程下进行。

thenApply 将在原始CompletableFuture 的线程中运行。要么是调用complete() 的线程,要么是调用thenApply() 的线程(如果future 已经完成)。如果你想控制线程——如果fn 是一个缓慢的操作是个好主意——那么你应该使用thenApplyAsync

【讨论】:

不是很清楚原始线程。如果未来由一个独立的线程池完成呢?例如,我们在池中执行一些计算,完成后只需调用CompletableFuture::complete 还要注意CompletableFuturethenApply调用返回之前完成的极端情况——在这种情况下,因为CompletableFuture已经完成;它将在 current 线程上执行。

以上是关于CompletableFuture 的完成处理程序在哪个线程中执行?的主要内容,如果未能解决你的问题,请参考以下文章

线程异步编排并行(CompletableFuture)

传递给 CompletableFuture.exceptionalally() 的异常处理程序是不是必须返回有意义的值?

CompletableFuture 异步编排

CompletableFuture使用详解

转载- CompletableFuture使用详解

如何将异步CompletableFuture与完成的CompletableFuture结合起来?