Верна ли причина автора использования thenCompose, а не thenComposeAsync - PullRequest
7 голосов
/ 02 августа 2020

Этот вопрос отличается от этого Разница между Java8 thenCompose и thenComposeAsyn c потому что я хочу знать, в чем причина использования автора thenCompose, а не thenComposeAsync.

Я читал Modern Java в действии и наткнулся на эту часть кода на странице 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());
}

Все в порядке, и я могу понять этот код, но вот причина автора, почему он не использовал thenComposeAsync на странице 408, которую я не могу понять:

В общем, метод без суффикса Asyn c в своем имени выполняет свою задачу в тех же потоках, что и предыдущий задача, тогда как метод, завершающийся с помощью Asyn c, всегда отправляет следующую задачу в пул потоков, поэтому каждая из задач может обрабатываться другим потоком. В этом случае результат второго CompletableFuture зависит от первого, поэтому не имеет значения для конечного результата или его времени sh, составляете ли вы два CompletableFutures с помощью одного или другого варианта этого метода

В моем понимании с подписями thenComposethenComposeAsync), как показано ниже:

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);
}

Результат второго CompletableFuture может зависеть от предыдущего CompletableFuture во многих ситуациях (или, скорее, я могу сказать почти всегда), должны ли мы использовать thenCompose, а не thenComposeAsync в этих случаях?

Что, если у нас есть код блокировки во втором CompletableFuture?

Это аналогичный пример, который был приведен человеком, который ответил на аналогичный вопрос здесь: Разница между Java8 thenCompose и thenComposeAsyn c

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

На мой взгляд, в этой ситуации использование thenComposeAsync может ускорить нашу программу, потому что здесь blockingRequestForQuote может выполняться в другом потоке. Но, по мнению автора, мы не должны использовать thenComposeAsync, потому что это зависит от первого CompletableFuture результата (то есть цитаты).

Мой вопрос:

Верна ли идея автора когда он сказал:

В этом случае результат второго CompletableFuture зависит от первого, поэтому не имеет значения ни окончательный результат, ни его широкое sh время, независимо от того, сочиняете ли вы два CompletableFutures с одним или другим вариантом этого метода

1 Ответ

6 голосов
/ 03 августа 2020

TL; DR Здесь правильно использовать thenCompose вместо thenComposeAsync, но не по указанным причинам. Как правило, пример кода не следует использовать в качестве шаблона для вашего собственного кода.

Эта глава является повторяющимся топом c на Stackoverflow по причинам, которые мы можем лучше всего описать как «недостаточное качество», чтобы оставаться вежливым.

В общем, метод без Asyn c суффикс в своем имени выполняет свою задачу в тех же потоках, что и предыдущая задача,…

В спецификации нет такой гарантии относительно выполняющегося потока. В документации говорится:

  • Действия, предоставляемые для зависимых завершений неасинхронных c методов, могут выполняться потоком, завершающим current CompletableFuture или любым другим вызывающим методом завершения.

Таким образом, существует также вероятность того, что задача выполняется «любым другим вызывающим методом завершения». Интуитивно понятный пример:

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

Здесь задействованы два потока. Один вызывает supplyAsync и thenApply, а другой - foo(). Если второй завершает вызов foo() до того, как первый поток входит в выполнение thenApply, возможно, что future уже завершено.

Future не помнит, какой поток его завершил. Также у него нет какой-то магической c способности указать этому потоку выполнить действие, несмотря на то, что он мог быть занят чем-то другим или даже с тех пор завершился. Таким образом, должно быть очевидно, что вызов thenApply в уже завершенном будущем не может обещать использовать поток, который его завершил. В большинстве случаев действие выполняется немедленно в потоке, вызывающем thenApply. Это покрывается формулировкой спецификации « любой другой вызывающий метод завершения ».

Но это еще не конец истории. Как этот ответ объясняет, когда задействовано более двух потоков, действие также может быть выполнено другим потоком, одновременно вызывающим не связанный метод завершения в будущем. Это может происходить редко, но это возможно в эталонной реализации и разрешено спецификацией.

Мы можем резюмировать это как: Методы без Asyn c обеспечивают наименьший контроль над потоком, который выполнит действие и может даже выполнить его прямо в вызывающем потоке, что приведет к синхронному поведению.

Так что они лучше всего, когда выполняющийся поток не имеет значения, и вы не надеетесь для выполнения фонового потока, то есть для коротких неблокирующих операций.

, тогда как метод, завершающийся с помощью Asyn c, всегда отправляет последующую задачу в пул потоков, поэтому каждая из задач может быть обработана другой веткой. В этом случае результат второго CompletableFuture зависит от первого,…

Когда вы выполняете

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

, задействованы три фьючерсы, поэтому не совсем понятно, какое будущее подразумевается под «вторым». supplyAsync отправляет действие и возвращает будущее. Отправка содержится в функции, переданной в thenCompose, которая вернет другое будущее.

Если вы использовали здесь thenComposeAsync, вы только указали, что выполнение supplyAsync должно быть отправлено в поток pool, вместо того, чтобы выполнять его непосредственно в завершающем потоке или «любом другом вызывающем методе завершения», например, непосредственно в потоке, вызывающем thenCompose.

Рассуждения о зависимостях здесь не имеют смысла. «, затем » всегда подразумевает зависимость. Если вы используете здесь thenComposeAsync, вы принудительно отправили действие в пул потоков, но эта отправка все равно не произойдет до завершения future. И если future завершено в исключительных случаях, отправки вообще не будет.

Итак, разумно ли здесь использовать thenCompose? Да, но цитата не по причинам, указанным. Как уже говорилось, использование метода non-asyn c подразумевает отказ от контроля над выполняющимся потоком и должно использоваться только тогда, когда поток не имеет значения, особенно для коротких неблокирующих действий. Вызов supplyAsync - дешевое действие, которое само по себе отправит фактическое действие в пул потоков, поэтому его можно выполнять в любом потоке, который свободен для этого.

Однако это ненужное осложнение. Вы можете добиться того же, используя

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

, который будет делать то же самое, отправить applyDiscount на executor, когда future будет завершен, и создать новое будущее, представляющее результат. Использование комбинации thenCompose и supplyAsync здесь не требуется.

Обратите внимание, что именно этот пример уже обсуждался в этих вопросах и ответах , который также решает ненужное разделение будущих операций по нескольким Stream операциям, а также неправильная диаграмма последовательности.

...