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 c1
和 c2
,而另一个线程不会调用任何一个。
仔细分析 JDK 代码可以确定上述假设场景的结果。但即使这样也是有风险的,因为您最终可能会依赖(1)不可移植或(2)可能更改的实现细节。最好不要假设 javadocs 或原始 JSR 规范中没有说明的任何内容。
tldr:要小心你的假设,并且在编写文档时,要尽可能清晰和深思熟虑。虽然简洁是一件美妙的事情,但要警惕人类填补空白的倾向。
【讨论】:
有趣的分析 - 真正挖掘并发编程领域中实现承诺的复杂性。 似乎在过去,当我阅读该文档时,我应该问自己“完成方法”究竟是什么意思。 “仔细分析 JDK 代码”得出的结论是,您描述的大多数令人惊讶的场景确实是可能的。因此,依赖实施细节的风险相当低。两个独立的动作没有顺序,因此不会按照它们注册的顺序执行,这一事实已经在here 进行了讨论,尽管这甚至不需要你描述的更令人惊讶的场景。 @Holger 我不喜欢他们使用“完成”来描述在完成其先行项后运行的任务的方式。因为这个词在讨论期货时经常出现(“完成”、“运行到完成”等),在 javadoc 摘录之类的上下文中很容易掩盖或误解它。我宁愿他们改用“延续”。 是的,当我第一次阅读时,我认为“完成方法”是指complete
、completeExceptionally
、cancel
或obtrude…
中的任何一个完整的而不是链或定义或延续……
@phant0m 不,它不适用于完全不相关的期货。【参考方案2】:
CompletableFuture
文档中指定的政策可以帮助您更好地理解:
为非异步方法的依赖完成提供的操作可能是 由完成当前 CompletableFuture 的线程执行, 或由完成方法的任何其他调用者。
执行所有没有显式 Executor 参数的异步方法 使用
ForkJoinPool.commonPool()
(除非它不支持 并行度至少为两个,在这种情况下,一个新的线程是 创建以运行每个任务)。为了简化监控、调试和 跟踪,所有生成的异步任务都是marker的实例 接口CompletableFuture.AsynchronousCompletionTask
。
更新:我还建议阅读@Mike 的this answer,作为对文档细节的进一步有趣分析。
【讨论】:
对于像thenApply
、thenRun
这样的方法,文档中的内容已经很清楚了。但是allOf
呢,对于fa = CompletableFuture.allOf(f0, f1, f2); fa.thenRun(someRunnable)
,假设f0
、f1
、f2
分别在线程A、线程B、线程C中完成,那么someRunnable
会在哪个线程中执行呢?同样,如果f0.thenCompose(x -> someNewCompletionStageProducer).thenRun(someRunnable)
、someRunnable
将在f0
的线程或fn
返回的未来中执行,那么thenCompose(Function<? super T,? extends CompletionStage<U>> fn)
呢? @纳曼【参考方案3】:
来自Javadoc:
为非异步方法的依赖完成提供的操作可以由完成当前 CompletableFuture 的线程执行,也可以由完成方法的任何其他调用者执行。
更具体地说:
fn
将在调用complete()
期间在调用complete()
的任何线程的上下文中运行。
如果在调用thenApply()
时complete()
已经完成,则fn
将在调用thenApply()
的线程上下文中运行。
【讨论】:
【参考方案4】:在线程方面缺少 API 文档。理解线程和期货是如何工作的需要一些推理。从一个假设开始:CompletableFuture
的非Async
方法不会自行生成新线程。工作将在现有线程下进行。
thenApply
将在原始CompletableFuture
的线程中运行。要么是调用complete()
的线程,要么是调用thenApply()
的线程(如果future 已经完成)。如果你想控制线程——如果fn
是一个缓慢的操作是个好主意——那么你应该使用thenApplyAsync
。
【讨论】:
不是很清楚原始线程。如果未来由一个独立的线程池完成呢?例如,我们在池中执行一些计算,完成后只需调用CompletableFuture::complete
。
还要注意CompletableFuture
在thenApply
调用返回之前完成的极端情况——在这种情况下,因为CompletableFuture
已经完成;它将在 current 线程上执行。以上是关于CompletableFuture 的完成处理程序在哪个线程中执行?的主要内容,如果未能解决你的问题,请参考以下文章
传递给 CompletableFuture.exceptionalally() 的异常处理程序是不是必须返回有意义的值?