简体   繁体   English

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

[英]In which thread do CompletableFuture's completion handlers execute?

I have a question about CompletableFuture method:我对 CompletableFuture 方法有疑问:

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

The thing is the JavaDoc says just this:问题是 JavaDoc 就是这样说的:

Returns a new CompletionStage that, when this stage completes normally, is executed with this stage's result as the argument to the supplied function.返回一个新的 CompletionStage,当此阶段正常完成时,将使用此阶段的结果作为所提供函数的参数来执行该阶段。 See the CompletionStage documentation for rules covering exceptional completion.有关异常完成的规则​​,请参阅 CompletionStage 文档。

What about threading?线程呢? In which thread is this going to be executed?这将在哪个线程中执行? What if the future is completed by a thread pool?如果future是由一个线程池完成的呢?

As @nullpointer points out, the documentation tells you what you need to know. 正如@nullpointer指出的那样,文档会告诉您您需要了解的内容。 However, the relevant text is surprisingly vague, and some of the comments (and answers) posted here seem to rely on assumptions that aren't supported by the documentation. 但是,相关文本令人惊讶地含糊,此处发布的某些评论(和答案)似乎依赖于文档不支持的假设。 Thus, I think it's worthwhile to pick it apart. 因此,我认为有必要将其分开。 Specifically, we should read this paragraph very carefully: 具体来说,我们应该非常仔细地阅读本段内容:

Actions supplied for dependent completions of non-async methods may be performed by the thread that completes the current CompletableFuture, or by any other caller of a completion method. 为非异步方法的相关完成提供的动作可以由完成当前CompletableFuture的线程执行,也可以由完成方法的任何其他调用者执行。

Sounds straightforward enough, but it's light on details. 听起来很简单,但是细节上却很少。 It seemingly deliberately avoids describing when a dependent completion may be invoked on the completing thread versus during a call to a completion method like thenApply . 似乎故意避免描述何时可以在完成线程上调用依赖完成,而不是在调用诸如thenApply这样的完成方法thenApply As written, the paragraph above is practically begging us to fill in the gaps with assumptions. 如前所述,以上段落实际上是在恳求我们用假设来填补空白。 That's dangerous, especially when the topic concerns concurrent and asynchronous programming, where many of the expectations we've developed as programmers get turned on their head. 这很危险,尤其是当主题涉及并发和异步编程时,在这种情况下,随着程序员的发展,我们已经产生了许多期望。 Let's take a careful look at what the documentation doesn't say. 让我们仔细看一下文档中没有说的内容。

The documentation does not claim that dependent completions registered before a call to complete() will run on the completing thread. 该文档没有声明调用complete() 之前注册的相关完成将在完成线程上运行。 Moreover, while it states that a dependent completion might be invoked when calling a completion method like thenApply , it does not state that a completion will be invoked on the thread that registers it (note the words "any other"). 此外,尽管它声明当调用诸如thenApply类的完成方法时可能会调用从属thenApply ,但并未声明将在注册它的线程上调用完成(请注意“其他”)。

These are potentially important points for anyone using CompletableFuture to schedule and compose tasks. 对于使用CompletableFuture计划和编写任务的任何人来说,这些都是潜在的重要点。 Consider this sequence of events: 考虑以下事件序列:

  1. Thread A registers a dependent completion via f.thenApply(c1) . 线程A通过f.thenApply(c1)注册依赖完成。
  2. Some time later, Thread B calls f.complete() . 一段时间后,线程B调用f.complete()
  3. Around the same time, Thread C registers another dependent completion via f.thenApply(c2) . 大约在同一时间,线程C通过f.thenApply(c2)注册另一个依赖完成。

Conceptually, complete() does two things: it publishes the result of the future, and then it attempts to invoke dependent completions. 从概念上讲, complete()做两件事:发布将来的结果,然后尝试调用相关的完成。 Now, what happens if Thread C runs after the result value is posted, but before Thread B gets around to invoking c1 ? 现在,如果线程C 发布结果值之后线程B开始调用c1 之前运行该怎么办? Depending on the implementation, Thread C may see that f has completed, and it may then invoke c1 and c2 . 根据实现的不同,线程C可能会看到f已经完成,然后可以调用c1 c2 Alternatively, Thread C may invoke c2 while leaving Thread B to invoke c1 . 或者,线程C可以调用c2而线程B可以调用c1 The documentation does not rule out either possibility. 该文档不排除任何可能性。 With that in mind, here are assumptions that are not supported by the documentation: 考虑到这一点,以下是文档不支持的假设:

  1. That a dependent completion c registered on f prior to completion will be invoked during the call to f.complete() ; 在对f.complete()调用期间,将在完成之前f注册的从属完成c被调用;
  2. That c will have run to completion by the time f.complete() returns; f.complete()返回时, c将运行完成。
  3. That dependent completions will be invoked in any particular order (eg, order of registration); 依赖完成将以任何特定顺序(例如注册顺序)被调用;
  4. That dependent completions registered before f completes will be invoked before completions registered after f completes. 之前注册的依赖完井f完成后,将注册落成之前被调用f完成。

Consider another example: 考虑另一个示例:

  1. Thread A calls f.complete() ; 线程A调用f.complete() ;
  2. Some time later, Thread B registers a completion via f.thenApply(c1) ; 一段时间后,线程B通过f.thenApply(c1)注册完成。
  3. Around the same time, Thread C registers a separate completion via f.thenApply(c2) . 大约在同一时间,线程C通过f.thenApply(c2)注册一个单独的完成。

If it is known that f has already run to completion, one might be tempted to assume that c1 will be invoked during f.thenApply(c1) and that c2 will be invoked during f.thenApply(c2) . 如果知道f已经运行完毕,则可能会倾向于假设c1将在f.thenApply(c1)期间被调用,而c2将在f.thenApply(c2)期间被调用。 One might further assume that c1 will have run to completion by the time f.thenApply(c1) returns. 可能还假定c1将在f.thenApply(c1)返回时运行完毕。 However, the documentation does not support these assumptions. 但是,文档支持这些假设。 It may be possible that one of the threads calling thenApply ends up invoking both c1 and c2 , while the other thread invokes neither. 这可能是可能的线程调用的一个 thenApply最终调用两个 c1c2 ,而其他线程调用都不是。

A careful analysis of the JDK code could determine how the hypothetical scenarios above might play out. 对JDK代码的仔细分析可以确定上面的假设方案如何发挥作用。 But even that is risky, because you may end up relying on an implementation detail that is (1) not portable, or (2) subject to change. 但这甚至是有风险的,因为您最终可能会依赖于(1)不可移植或(2)随时可能更改的实现细节。 Your best bet is not to assume anything that's not spelled out in the javadocs or the original JSR spec. 最好的选择是不要假设javadocs或原始JSR规范中未阐明的任何内容。

tldr: Be careful what you assume, and when you write documentation, be as clear and deliberate as possible. tldr:谨慎假设,编写文档时,请尽可能清晰明了。 While brevity is a wonderful thing, be wary of the human tendency to fill in the gaps. 简洁是一件奇妙的事情,但请注意人类填补空白的趋势。

The policies as specified in the CompletableFuture docs could help you understand better: CompletableFuture文档中指定的策略可以帮助您更好地理解:

  • Actions supplied for dependent completions of non-async methods may be performed by the thread that completes the current CompletableFuture , or by any other caller of a completion method . 为非异步方法的相关完成提供的动作可以由完成当前CompletableFuture线程执行,也可以由完成方法的任何其他调用者执行

  • All async methods without an explicit Executor argument are performed using the ForkJoinPool.commonPool() (unless it does not support a parallelism level of at least two, in which case, a new Thread is created to run each task ). 所有没有显式Executor参数的异步方法都是使用ForkJoinPool.commonPool()执行的(除非它不支持并行度至少为2,在这种情况下,将创建一个新的Thread来运行每个任务 )。 To simplify monitoring, debugging, and tracking, all generated asynchronous tasks are instances of the marker interface CompletableFuture.AsynchronousCompletionTask . 为了简化监视,调试和跟踪,所有生成的异步任务都是标记接口CompletableFuture.AsynchronousCompletionTask实例。

Update : I would also advice on reading this answer by @Mike as an interesting analysis further into the details of the documentation. 更新 :我也建议阅读@Mike的答案 ,作为对文档详细信息的有趣分析。

From the Javadoc : Javadoc

Actions supplied for dependent completions of non-async methods may be performed by the thread that completes the current CompletableFuture, or by any other caller of a completion method. 为非异步方法的相关完成提供的动作可以由完成当前CompletableFuture的线程执行,也可以由完成方法的任何其他调用者执行。

More concretely: 更具体地说:

  • fn will run during the call to complete() in the context of whichever thread has called complete() . fn将在调用complete()的上下文中运行,无论哪个线程调用了complete()

  • If complete() has already finished by the time thenApply() is called, fn will be run in the context of the thread calling thenApply() . 如果complete()已经由时间完成thenApply()被调用时, fn将在线程中调用的上下文中运行thenApply()

When it comes to threading the API documentation is lacking. 当涉及线程时,缺少API文档。 It takes a bit of inference to understand how threading and futures work. 要了解线程和期货的工作方式需要一点推论。 Start with one assumption: the non- Async methods of CompletableFuture do not spawn new threads on their own. 从一个假设开始: CompletableFuture的非Async方法不会自行产生新线程。 Work will proceed under existing threads. 工作将在现有线程下进行。

thenApply will run in the original CompletableFuture 's thread. thenApply将在原始CompletableFuture的线程中运行。 That's either the thread that calls complete() , or the one that calls thenApply() if the future is already completed. 如果将来已经完成,那要么是调用complete()的线程,要么是调用thenApply()的线程。 If you want control over the thread—a good idea if fn is a slow operation—then you should use thenApplyAsync . 如果要控制线程(如果fn是缓慢的操作,则是个好主意),则应使用thenApplyAsync

I know this question is old, but I want to use source code to explain this question.我知道这个问题很老,但我想用源代码来解释这个问题。

public CompletableFuture<Void> thenAccept(Consumer<? super T> action) {
    return uniAcceptStage(null, action);
}

private CompletableFuture<Void> uniAcceptStage(Executor e,
                                               Consumer<? super T> f) {
    if (f == null) throw new NullPointerException();
    Object r;
    if ((r = result) != null)
        return uniAcceptNow(r, e, f);
    CompletableFuture<Void> d = newIncompleteFuture();
    unipush(new UniAccept<T>(e, d, this, f));
    return d;
}

This is the source code from java 16, and we can see, if we trigger thenAccept, we will pass a null executor service reference into our function.这是来自 java 16 的源代码,我们可以看到,如果我们触发 thenAccept,我们会将一个空的执行器服务引用传递给我们的函数。 From the 2nd function uniAcceptStage() 2nd if condition.从第二个函数 uniAcceptStage() 第二个 if 条件。 If result is not null, it will trigger uniAcceptNow()如果结果不为空,它将触发 uniAcceptNow()

if (e != null) {
     e.execute(new UniAccept<T>(null, d, this, f));
} else {
     @SuppressWarnings("unchecked") T t = (T) r;
     f.accept(t);
     d.result = NIL;
}

if executor service is null, we will use lambda function f.accept(t) to execute it.如果执行器服务为空,我们将使用 lambda 函数 f.accept(t) 来执行它。 If we are triggering this thenApply/thenAccept from main thread, it will use main thread as executing thread.如果我们从主线程触发 thenApply/thenAccept,它将使用主线程作为执行线程。

But if we cannot get previous result from last completablefuture, we will push our current UniAccept/Apply into stack by using uniPush function.但是,如果我们无法从上一个可完成的未来中获得先前的结果,我们将使用 uniPush 函数将当前的 UniAccept/Apply 推入堆栈。 And UniAccept class has tryFire() which will be triggered from our postComplete() function UniAccept 类有 tryFire() 将由我们的 postComplete() 函数触发

final void postComplete() {
    /*
     * On each step, variable f holds current dependents to pop
     * and run.  It is extended along only one path at a time,
     * pushing others to avoid unbounded recursion.
     */
    CompletableFuture<?> f = this; Completion h;
    while ((h = f.stack) != null ||
           (f != this && (h = (f = this).stack) != null)) {
        CompletableFuture<?> d; Completion t;
        if (STACK.compareAndSet(f, h, t = h.next)) {
            if (t != null) {
                if (f != this) {
                    pushStack(h);
                    continue;
                }
                NEXT.compareAndSet(h, t, null); // try to detach
            }
            f = (d = h.tryFire(NESTED)) == null ? this : d;
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM