简体   繁体   English

作者使用 thenCompose 而不是 thenComposeAsync 的理由是否正确

[英]Is the writer's reason correct for using thenCompose and not thenComposeAsync

This question is different from this one Difference between Java8 thenCompose and thenComposeAsync because I want to know what is the writer's reason for using thenCompose and not thenComposeAsync .这个问题不同于这个Difference between Java8 thenCompose and thenComposeAsync因为我想知道作者使用thenCompose而不是thenComposeAsync的原因是什么。

I was reading Modern Java in action and I came across this part of code on page 405:我正在阅读 Modern Java in action,我在第 405 页看到了这部分代码:

public static List<String> findPrices(String product) {
    ExecutorService executor = Executors.newFixedThreadPool(10);
    List<Shop> shops = Arrays.asList(new Shop(), new Shop());
    List<CompletableFuture<String>> priceFutures = shops.stream()
            .map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice(product), executor))
            .map(future -> future.thenApply(Quote::parse))
            .map(future -> future.thenCompose(quote ->
                    CompletableFuture.supplyAsync(() -> Discount.applyDiscount(quote), executor)))
            .collect(toList());
    return priceFutures.stream()
            .map(CompletableFuture::join).collect(toList());
}

Everything is Ok and I can understand this code but here is the writer's reason for why he didn't use thenComposeAsync on page 408 which I can't understand:一切都很好,我可以理解这段代码,但这是作者在第 408 页上不使用thenComposeAsync的原因,我无法理解:

In general, a method without the Async suffix in its name executes its task in the same threads the previous task, whereas a method terminating with Async always submits the succeeding task to the thread pool, so each of the tasks can be handled by a different thread.通常,名称中没有 Async 后缀的方法会在前一个任务的相同线程中执行其任务,而以 Async 终止的方法总是将后续任务提交给线程池,因此每个任务都可以由不同的线程处理线。 In this case, the result of the second CompletableFuture depends on the first,so it makes no difference to the final result or to its broad-brush timing whether you compose the two CompletableFutures with one or the other variant of this method在这种情况下,第二个 CompletableFuture 的结果取决于第一个,因此无论您将两个 CompletableFutures 与此方法的一个或另一个变体组合在一起,对最终结果或其粗略计时都没有影响

In my understanding with the thenCompose ( and thenComposeAsync ) signatures as below:根据我对thenCompose (和thenComposeAsync )签名的理解,如下所示:

public <U> CompletableFuture<U> thenCompose(
    Function<? super T, ? extends CompletionStage<U>> fn) {
    return uniComposeStage(null, fn);
}

public <U> CompletableFuture<U> thenComposeAsync(
    Function<? super T, ? extends CompletionStage<U>> fn) {
    return uniComposeStage(asyncPool, fn);
}

The result of the second CompletableFuture can depends on the previous CompletableFuture in many situations(or rather I can say almost always), should we use thenCompose and not thenComposeAsync in those cases?在许多情况下(或者我可以说几乎总是),第二个CompletableFuture的结果可能取决于之前的CompletableFuture ,在这些情况下我们应该使用thenCompose而不是thenComposeAsync吗?

What if we have blocking code in the second CompletableFuture ?如果我们在第二个CompletableFuture中有阻塞代码怎么办?

This is a similar example which was given by person who answered similar question here: Difference between Java8 thenCompose and thenComposeAsync这是一个类似的例子,由在这里回答类似问题的人给出: Difference between Java8 thenCompose and thenComposeAsync

public CompletableFuture<String> requestData(Quote quote) {
    Request request = blockingRequestForQuote(quote);
    return CompletableFuture.supplyAsync(() -> sendRequest(request));
}

To my mind in this situation using thenComposeAsync can make our program faster because here blockingRequestForQuote can be run on different thread.在我看来,在这种情况下使用thenComposeAsync可以使我们的程序更快,因为这里blockingRequestForQuote可以在不同的线程上运行。 But based on the writer's opinion we should not use thenComposeAsync because it depends on the first CompletableFuture result(that is Quote).但是根据作者的意见,我们不应该使用thenComposeAsync ,因为它取决于第一个CompletableFuture结果(即 Quote)。

My question is:我的问题是:

Is the writer's idea correct when he said:作者的想法是否正确,他说:

In this case, the result of the second CompletableFuture depends on the first,so it makes no difference to the final result or to its broad-brush timing whether you compose the two CompletableFutures with one or the other variant of this method在这种情况下,第二个 CompletableFuture 的结果取决于第一个,因此无论您将两个 CompletableFutures 与此方法的一个或另一个变体组合在一起,对最终结果或其粗略计时都没有影响

TL;DR It is correct to use thenCompose instead of thenComposeAsync here, but not for the cited reasons. TL;DR在这里使用thenCompose而不是thenComposeAsync是正确的,但不是出于引用的原因。 Generally, the code example should not be used as a template for your own code.通常,不应将代码示例用作您自己的代码的模板。


This chapter is a recurring topic on Stackoverflow for reasons we can best describe as “insufficient quality”, to stay polite.本章是 Stackoverflow 上反复出现的主题,为了保持礼貌,我们最好将其描述为“质量不足”。

In general, a method without the Async suffix in its name executes its task in the same threads the previous task, …通常,名称中没有 Async 后缀的方法会在与前一个任务相同的线程中执行其任务,...

There is no such guaranty about the executing thread in the specification.规范中没有关于执行线程的此类保证。 The documentation says: 文件说:

  • 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 的线程或完成方法的任何其他调用者执行。

So there's also the possibility that the task is performed “by any other caller of a completion method”.所以也有可能任务是“由完成方法的任何其他调用者”执行的。 An intuitive example is一个直观的例子是

CompletableFuture<X> f = CompletableFuture.supplyAsync(() -> foo())
    .thenApply(f -> f.bar());

There are two threads involved.涉及两个线程。 One that invokes supplyAsync and thenApply and the other which will invoke foo() .一个调用supplyAsyncthenApply ,另一个调用foo() If the second completes the invocation of foo() before the first thread enters the execution of thenApply , it is possible that the future is already completed.如果第二个线程在第一个线程进入thenApply执行之前完成了foo()的调用,则有可能 future 已经完成。

A future does not remember which thread completed it.未来不记得哪个线程完成了它。 Neither does it have some magic ability to tell that thread to perform an action despite it might be busy with something else or even have terminated since then.它也没有一些神奇的能力来告诉该线程执行一个动作,尽管它可能正忙于其他事情,甚至从那时起就已经终止了。 So it should be obvious that calling thenApply on an already completed future can't promise to use the thread that completed it.所以很明显,在已经完成的未来调用thenApply不能 promise 使用完成它的线程。 In most cases, it will perform the action immediately in the thread that calls thenApply .在大多数情况下,它会在调用thenApply的线程中立即执行操作。 This is covered by the specification's wording “ any other caller of a completion method ”.这包含在规范的措辞“完成方法的任何其他调用者”中。

But that's not the end of the story.但这还没有结束。 As this answer explains, when there are more than two threads involved, the action can also get performed by another thread calling an unrelated completion method on the future at the same time.正如这个答案所解释的那样,当涉及两个以上的线程时,该操作也可以由另一个线程同时在未来调用不相关的完成方法来执行。 This may happen rarely, but it's possible in the reference implementation and permitted by the specification.这可能很少发生,但在参考实现中是可能的,并且是规范允许的。

We can summarize it as: Methods without Async provides the least control over the thread that will perform the action and may even perform it right in the calling thread, leading to synchronous behavior.我们可以将其总结为:没有Async的方法对将执行操作的线程提供的控制最少,甚至可能在调用线程中执行它,从而导致同步行为。

So they are best when the executing thread doesn't matter and you're not hoping for background thread execution, ie for short, non-blocking operations.因此,当执行线程无关紧要并且您不希望后台线程执行时,它们是最好的,即短的非阻塞操作。

whereas a method terminating with Async always submits the succeeding task to the thread pool, so each of the tasks can be handled by a different thread.而以 Async 终止的方法总是将后续任务提交给线程池,因此每个任务都可以由不同的线程处理。 In this case, the result of the second CompletableFuture depends on the first, …在这种情况下,第二个 CompletableFuture 的结果取决于第一个,......

When you do当你做

future.thenCompose(quote ->
    CompletableFuture.supplyAsync(() -> Discount.applyDiscount(quote), executor))

there are three futures involved, so it's not exactly clear, which future is meant by “second”.涉及三个future,所以不清楚“second”指的是哪个future。 supplyAsync is submitting an action and returning a future. supplyAsync正在提交一个动作并返回一个未来。 The submission is contained in a function passed to thenCompose , which will return another future.提交包含在传递给thenCompose的 function 中,它将返回另一个未来。

If you used thenComposeAsync here, you only mandated that the execution of supplyAsync has to be submitted to the thread pool, instead of performing it directly in the completing thread or “any other caller of a completion method”, eg directly in the thread calling thenCompose .如果你在这里使用thenComposeAsync ,你只要求supplyAsync的执行必须提交给线程池,而不是直接在完成线程或“完成方法的任何其他调用者”中执行,例如直接在调用thenCompose的线程中执行.

The reasoning about dependencies makes no sense here.关于依赖关系的推理在这里没有意义。 then ” always implies a dependency. 然后”总是暗示着依赖。 If you use thenComposeAsync here, you enforced the submission of the action to the thread pool, but this submission still won't happen before the completion of future .如果你在这里使用thenComposeAsync ,你就强制将动作提交到线程池,但是这个提交在future完成之前仍然不会发生。 And if future completed exceptionally, the submission won't happen at all.如果future异常完成,则提交根本不会发生。

So, is using thenCompose reasonable here?那么,这里使用thenCompose合理吗? Yes it is, but not for the reasons given is the quote.是的,但不是因为给出的原因是报价。 As said, using the non-async method implies giving up control over the executing thread and should only be used when the thread doesn't matter, most notably for short, non-blocking actions.如前所述,使用非异步方法意味着放弃对正在执行的线程的控制,并且只应在线程无关紧要时使用,最明显的是用于简短的非阻塞操作。 Calling supplyAsync is a cheap action that will submit the actual action to the thread pool on its own, so it's ok to perform it in whatever thread is free to do it.调用supplyAsync是一种廉价的操作,它会自行将实际操作提交给线程池,因此可以在任何空闲的线程中执行它。

However, it's an unnecessary complication.然而,这是一个不必要的并发症。 You can achieve the same using你可以实现相同的使用

future.thenApplyAsync(quote -> Discount.applyDiscount(quote), executor)

which will do exactly the same, submit applyDiscount to executor when future has been completed and produce a new future representing the result.这将做完全相同的事情,当future完成时将applyDiscount提交给executor并产生一个代表结果的新 future。 Using a combination of thenCompose and supplyAsync is unnecessary here.此处不需要结合使用thenComposesupplyAsync

Note that this very example has been discussed in this Q&A already, which also addresses the unnecessary segregation of the future operations over multiple Stream operations as well as the wrong sequence diagram.请注意,这个例子已经在这个问答中讨论过,它还解决了未来操作在多个Stream操作上的不必要的分离以及错误的序列图。

What a polite answer from Holger.霍尔格的回答多么礼貌。 I am really impressed he could provide such a great explanation and at the same time staying in bounds of not calling the author plain wrong.他能提供如此出色的解释,同时保持不说作者明显错误的界限,给我留下了深刻的印象。 I want to provide my 0,02$ here too, a little.我也想在这里提供我的 0,02$,一点点。 after reading the same book and having to scratch my head twice.在阅读同一本书并且不得不两次挠头之后。

First of all, there is no "remembering" of which thread executed which stage, neither does the specification make such a statement (as already answered above).首先,没有“记住”哪个线程执行了哪个阶段,规范也没有做出这样的声明(如上所述)。 The interesting part is even in the cited above documentation:有趣的部分甚至在上面引用的文档中:

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 的线程或完成方法的任何其他调用者执行。

Even that ...completes the current CompletableFuture part is tricky.即使这样...完成当前的 CompletableFuture部分也很棘手。 What if there are two threads that try to call complete on a CompletableFuture , which thread will run all the dependent actions?如果有两个线程尝试在CompletableFuture上调用complete ,那么哪个线程将运行所有相关操作呢? The one that has actually completed it?真正完成的那个? Or any other?或者其他的? I wrote a jcstress test that is very non-intuitive when looking at the results:我写了一个jcstress测试,在查看结果时非常不直观:

@JCStressTest
@State
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE, desc = "executed in completion thread")
@Outcome(id = "0, 1", expect = Expect.ACCEPTABLE, desc = "executed in the other thread")
@Outcome(id = "0, 0", expect = Expect.FORBIDDEN)
@Outcome(id = "1, 1", expect = Expect.FORBIDDEN)
public class CompletableFutureWhichThread1 {

    private final CompletableFuture<String> future = new CompletableFuture<>();

    public CompletableFutureWhichThread1() {
        future.thenApply(x -> action(Thread.currentThread().getName()));
    }

    volatile int x = -1; // different default to not mess with the expected result
    volatile int y = -1; // different default to not mess with the expected result
    volatile int actor1 = 0;
    volatile int actor2 = 0;

    private String action(String threadName) {
        System.out.println(Thread.currentThread().getName());
        // same thread that completed future, executed action
        if ("actor1".equals(threadName) && actor1 == 1) {
            x = 1;
            return "action";
        }

        // same thread that completed future, executed action
        if ("actor2".equals(threadName) && actor2 == 1) {
            x = 1;
            return "action";
        }

        y = 1;
        return "action";

    }

    @Actor
    public void actor1() {
        Thread.currentThread().setName("actor1");
        boolean completed = future.complete("done-actor1");
        if (completed) {
            actor1 = 1;
        } else {
            actor2 = 1;
        }
    }

    @Actor
    public void actor2() {
        Thread.currentThread().setName("actor2");
        boolean completed = future.complete("done-actor2");
        if (completed) {
            actor2 = 1;
        }
    }

    @Arbiter
    public void arbiter(II_Result result) {
        if (x == 1) {
            result.r1 = 1;
        }

        if (y == 1) {
            result.r2 = 1;
        }

    }

}

After running this, both 0, 1 and 1, 0 are seen.运行此命令后,可以看到0, 11, 0 You do not need to understand very much about the test itself, but it proves a rather interesting point.您不需要对测试本身了解太多,但它证明了一个相当有趣的观点。

You have a CompletableFuture future that has a future.thenApply(x -> action(...));你有一个CompletableFuture future ,它有一个future.thenApply(x -> action(...)); attached to it.附加到它。 There are two threads ( actor1 and actor2 ) that both, at the same time, compete with each other into completing it (the specification says that only one will be successful).有两个线程( actor1actor2 )同时相互竞争完成它(规范说只有一个会成功)。 The results show that if actor1 called complete , but does not actually complete the CompletableFuture ( actor2 did), it can still do the actual work in action .结果表明,如果actor1调用complete ,但实际上并没有完成CompletableFutureactor2做了),它仍然可以在action中完成实际工作。 In other words, a thread that completed a CompletableFuture is not necessarily the thread that executes the dependent actions (those thenApply for example).换句话说,完成CompletableFuture的线程不一定是执行相关操作的线程(例如那些thenApply )。 This was rather interesting for me to find out, though it makes sense.这对我来说很有趣,尽管它是有道理的。


Your reasonings about speed are a bit off.你对速度的推理有点不对劲。 When you dispatch your work to a different thread, you usually pay a penalty for that.当您将工作分派到不同的线程时,您通常会为此付出代价。 thenCompose vs thenComposeAsync is about being able to predict where exactly is your work going to happen. thenComposethenComposeAsync的区别在于能够准确预测您的工作将在何处进行。 As you have seen above you can not do that, unless you use the ...Async methods that take a thread pool.正如您在上面看到的,您不能这样做,除非您使用采用线程池的...Async方法。 Your natural question should be: "Why do I care where it is executed?".你的自然问题应该是:“我为什么要关心它在哪里执行?”。

There is an internal class in jdk's HttpClient called SelectorManager . jdk's HttpClient中有一个内部的 class 叫做SelectorManager It has (from a high level) a rather simple task: it reads from a socket and gives "responses" back to the threads that wait for a http result.它(从高层次上)有一个相当简单的任务:它从套接字中读取并将“响应”返回给等待 http 结果的线程。 In essence, this is a thread that wakes up all interested parties that wait for some http packets.本质上,这是一个唤醒所有等待一些 http 数据包的相关方的线程。 Now imagine that this particular thread does internally thenCompose .现在想象一下这个特定的线程在内部thenCompose Now also imagine that your chain of calls looks like this:现在也想象一下您的调用链如下所示:

 httpClient.sendAsync(() -> ...)
           .thenApply(x -> foo())

where foo is a method that never finishes (or takes a lot of time to finish).其中foo是一种永远不会完成(或需要很长时间才能完成)的方法。 Since you have no idea in which thread the actual execution is going to happen, it can, very well, happen in SelectorManager thread.由于您不知道实际执行将在哪个线程中发生,因此它很可能在SelectorManager线程中发生。 Which would be a disaster.这将是一场灾难。 Everyone other http calls would stale, because this thread is busy now.其他所有人 http 电话都会过时,因为这个线程现在很忙。 Thus thenComposeAsync : let the configured pool do the work/waiting if needed, while the SelectorManager thread is free to do its work.因此thenComposeAsync :如果需要,让配置的池执行工作/等待,而SelectorManager线程可以自由执行其工作。

So the reasons that the author gives are plain wrong.所以作者给出的理由是完全错误的。

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

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