В каком потоке выполняются обработчики CompletableFuture завершения?

У меня вопрос о методе CompletableFuture:

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

Дело в том, что JavaDoc говорит только следующее:

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

Как насчет потоков? В каком потоке это будет выполнено? Что, если будущее будет завершено пулом потоков?

Ответ 1

Политики, указанные в CompletableFuture, могут помочь вам лучше понять:

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

  • Все асинхронные методы без явного аргумента Executor выполняются используя ForkJoinPool.commonPool() (если он не поддерживает parallelism уровня не менее двух, и в этом случае новый поток будет созданные для выполнения каждой задачи). Для упрощения мониторинга, отладки и отслеживание, все сгенерированные асинхронные задачи являются экземплярами маркера интерфейс CompletableFuture.AsynchronousCompletionTask.

Обновление. Я также советую читать этот ответ от @Mike как интересный анализ далее в деталях документации.

Ответ 2

Как указывает @nullpointer, документация сообщает вам, что вам нужно знать. Тем не менее, соответствующий текст является на удивление неопределенным, и некоторые из комментариев (и ответов), размещенных здесь, похоже, полагаются на предположения, которые не подтверждаются документацией. Таким образом, я считаю целесообразным выделить его. В частности, мы должны внимательно прочитать этот параграф:

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

Звучит достаточно просто, но он освещает детали. По-видимому, он сознательно избегает описания того, когда зависимое завершение может быть вызвано в завершающем потоке или во время вызова метода завершения, такого как thenApply. Как написано, вышеприведенный пункт фактически попросит нас заполнить пробелы предположениями. Это опасно, особенно когда речь идет о параллельном и асинхронном программировании, где многие из ожиданий, которые мы разработали, когда программисты оказываются на их голове. Давайте внимательно рассмотрим, что говорит .

В документации не утверждается, что зависимые доработки, зарегистрированные до вызова complete(), будут выполняться в завершающей цепочке. Более того, в то время как в нем указано, что зависимое завершение может быть вызвано при вызове метода завершения, такого как thenApply, он не указывает, что завершение будет вызвано в потоке, который регистрирует его (обратите внимание на слова "любой другой" ).

Это потенциально важные моменты для тех, кто использует CompletableFuture для планирования и составления задач. Рассмотрим эту последовательность событий:

  • Резьба A регистрирует зависимое завершение через f.thenApply(c1).
  • Через некоторое время Thread B вызывает f.complete().
  • Примерно в то же время Thread C регистрирует другое зависимое завершение через f.thenApply(c2).

Концептуально complete() выполняет две вещи: публикует результат будущего, а затем пытается вызвать зависимые доработки. Теперь, что произойдет, если Thread C запускается после публикации значения результата, но до того, как Thread B приблизится к вызову c1? В зависимости от реализации Thread C может видеть, что f завершен, и он может затем вызвать c1 и c2. Альтернативно, Thread C может вызывать c2, оставив Thread B для вызова c1. Документация не исключает возможности. Имея это в виду, здесь приведены предположения, что не поддерживается по документации:

  • Завершение зависимого завершения c, зарегистрированное на f до завершения, будет вызываться во время вызова f.complete();
  • То, что c будет завершено к моменту возврата f.complete();
  • Эти зависимые дополнения будут вызываться в любом конкретном порядке (например, порядок регистрации);
  • Завершены все зависимые завершения, зарегистрированные до завершения f, до завершения завершений после завершения f.

Рассмотрим еще один пример:

  • Thread A вызывает f.complete();
  • Через некоторое время Thread B регистрирует завершение через f.thenApply(c1);
  • Примерно в то же время Thread C регистрирует отдельное завершение через f.thenApply(c2).

Если известно, что f уже завершено, может возникнуть соблазн предположить, что c1 будет вызываться во время f.thenApply(c1) и что c2 будет вызываться во время f.thenApply(c2). Можно также предположить, что c1 будет завершено к моменту возврата f.thenApply(c1). Однако документация не поддерживает эти допущения. Возможно, что один из потоков, вызывающих thenApply, завершает вызов как c1, так и c2, тогда как другой поток не вызывает никого.

Тщательный анализ кода JDK мог бы определить, как можно проиллюстрировать гипотетические сценарии выше. Но даже это рискованно, потому что вы можете положиться на детали реализации, которые (1) не переносимы, или (2) могут быть изменены. Лучше всего не предполагать ничего, что не указано в javadocs или исходной спецификации JSR.

TL;DR: Будьте осторожны с тем, что вы предполагаете, и когда вы пишете документацию, будьте как можно яснее и продуманнее. Хотя краткость - замечательная вещь, будьте осторожны с человеческой склонностью заполнить пробелы.

Ответ 3

Из Javadoc:

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

Более конкретно:

  • fn будет выполняться во время вызова complete() в контексте того, что нить вызвала complete().

  • Если complete() уже завершено к моменту вызова thenApply(), fn будет выполняться в контексте потока, вызывающего thenApply().

Ответ 4

Когда дело доходит до потоковой передачи, документация API отсутствует. Для понимания того, как работают потоки и фьючерсы, требуется немного вывода. Начните с одного предположения: CompletableFuture не порождает новые потоки самостоятельно. Работа будет продолжаться в рамках существующих потоков.

thenApply будет запущен в исходном потоке CompletableFuture. Это либо поток, который вызывает complete(), либо тот, который вызывает thenApply(), если будущее уже завершено. Если вы хотите контролировать поток — хорошая идея, если fn - медленная операция &mdash, тогда вы должны использовать thenApplyAsync.