Асинхронное выполнение HTTP-клиента Java 11

Я пробую новый клиентский API HTTP из JDK 11, особенно его асинхронный способ выполнения запросов. Но есть кое-что, что я не уверен, что понимаю (что-то вроде аспекта реализации). В документация, там сказано:

Асинхронные задачи и зависимые действия возвращенных экземпляров CompletableFuture выполняются в потоках, предоставляемых Executor клиента, где это целесообразно.

Насколько я понимаю, это означает, что если я задаю кастомный исполнитель при создании объекта HttpClient:

ExecutorService executor = Executors.newFixedThreadPool(3);

HttpClient httpClient = HttpClient.newBuilder()
                      .executor(executor)  // custom executor
                      .build();

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

httpClient.sendAsync(request, BodyHandlers.ofString())
          .thenAccept(response -> {
      System.out.println("Thread is: " + Thread.currentThread().getName());
      // do something when the response is received
});

Однако в вышеприведенном зависимом действии (потребитель в thenAccept) я вижу, что поток, выполняющий это, относится к общему пулу, а не к пользовательскому исполнителю, поскольку он печатает Thread is: ForkJoinPool.commonPool-worker-5.

Это ошибка в реализации? Или я что-то упускаю? Я заметил, что в нем говорится: «Экземпляры выполняются в потоках, предоставленных исполнителем клиента, там, где это целесообразно», так что это тот случай, когда это не применяется?

Обратите внимание, что я также пробовал thenAcceptAsync, и результат тот же.


person M A    schedule 18.08.2018    source источник
comment
извините, если это глупо, но помогите мне понять, как вы интерпретировали это из общего пула, а не пользовательского исполнителя, поскольку он печатает Thread is: ForkJoinPool.commonPool-worker-5?... Я также попробовал System.out.println(httpClient.executor().get().equals(executor)); в потребителе thenAccept, и он печатает true.   -  person Naman    schedule 18.08.2018
comment
@nullpointer Я предполагаю, что он распечатал Thread.currentThread().getName() внутри thenAccept Consumer, а имя указывает, что Thread взято из обычного ForkJoinPool, а не из пользовательского Executor. Другими словами, OP не говорит, что Executor из HttpClient изменился, OP задается вопросом, почему зависимый этап CompletableFuture выполняется с использованием другого пула потоков.   -  person Slaw    schedule 18.08.2018
comment
@nullpointer Именно то, что сказал Слоу. Я также знаю, что поток из общего пула, потому что я могу дать потокам, созданным пользовательским исполнителем, специальные имена, чтобы четко идентифицировать их. Что касается httpClient.executor(), этот метод просто возвращает исполнителя, который я указал при создании, а не то, что использует thenAccept.   -  person M A    schedule 18.08.2018
comment
@Slaw @manouti Спасибо. Я понял, на что вы оба указывали, действительно попытался предоставить исполнителю пользовательский именованный поток и увидел, что он не используется в thenAccept. Буду искать дополнительные сведения о там, где это целесообразно, а также о базе данных ошибок.   -  person Naman    schedule 18.08.2018
comment
Оказывается, документация уже была обновлена ​​во время работы над этим API, так что она описывает это поведение. Более свежая ссылка на документы: download.java.net/java/early_access/jdk11/docs/api/   -  person M A    schedule 23.08.2018


Ответы (2)


Я только что нашел обновленный документация (та, на которую я изначально ссылался, кажется старой), где объясняется такое поведение реализации:

Как правило, асинхронные задачи выполняются в любом потоке, вызывающем операцию, например. отправив HTTP-запрос, или потоками, предоставляемыми исполнитель. Зависимые задачи, запускаемые возвращаемыми CompletionStages или CompletableFuture, в которых явно не указан исполнитель, выполняются в одном и том же исполнитель по умолчанию, как у CompletableFuture, или вызывающий поток, если операция завершается до регистрации зависимой задачи.

И исполнителем по умолчанию CompletableFuture является общий пул.

Я также нашел идентификатор ошибки, который представляет это поведение, в котором разработчики API полностью объясняет это:

2) Зависимые задачи запускаются в общем пуле Выполнение зависимых задач по умолчанию было обновлено для выполнения в том же исполнителе, что и у defaultExecutor CompletableFuture. Это более знакомо разработчикам, которые уже используют CF, и снижает вероятность того, что HTTP-клиенту не хватит потоков для выполнения своих задач. Это просто поведение по умолчанию, и HTTP-клиент, и CompletableFuture при необходимости допускают более тонкий контроль.

person M A    schedule 23.08.2018
comment
«Это просто поведение по умолчанию, и HTTP-клиент, и CompletableFuture при необходимости допускают более точное управление.». Что касается CompletableFuture, я предполагаю, что он имеет в виду использование вариантов метода *Async(…, Executor). Однако для HTTP-клиента я не понимаю, как это помогает контролировать это поведение. Я не нахожу использование вариантов асинхронного метода везде очень удобным. - person Didier L; 23.08.2018
comment
Я тоже так думал, когда читал этот пункт; только билдер позволяет передавать исполнителя в HTTP-клиенте, что даже не влияет на зависимые задачи в CF. Кроме того, даже недавняя документация не кажется последовательной, поскольку он по-прежнему говорит Устанавливает исполнитель для использования для асинхронных и зависимых задач. Возможно, они пропустили обновление этой части. - person M A; 24.08.2018
comment
Я проголосовал за ответ @manouti, который попал в точку. Также была зарегистрирована следующая ошибка, чтобы исправить ошибочную спецификацию, в которой говорится, что зависимые задачи выполняются в исполнителе, bugs.openjdk.java.net/browse/JDK-8209943 - person chegar999; 24.08.2018
comment
@chegar999 Круто! И спасибо за работу по созданию этого API! К вашему сведению, я обобщил некоторые примеры использования API в сообщении здесь., во время которого я столкнулся с таким поведением. - person M A; 24.08.2018

Краткая версия: я думаю, вы определили детали реализации, и что "там, где это целесообразно" означает, что нет никакой гарантии, что предоставленный executor будет использоваться.

В деталях:

Я скачал исходный код JDK 11 с здесь. (jdk11-f729ca27cf9a на момент написания этой статьи).

В src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java есть следующий класс:

/**
 * A DelegatingExecutor is an executor that delegates tasks to
 * a wrapped executor when it detects that the current thread
 * is the SelectorManager thread. If the current thread is not
 * the selector manager thread the given task is executed inline.
 */
final static class DelegatingExecutor implements Executor {

Этот класс использует executor, если isInSelectorThread истинно, в противном случае задача выполняется встроенно. Это сводится к:

boolean isSelectorThread() {
    return Thread.currentThread() == selmgr;
}

где selmgr это SelectorManager. Изменить: этот класс также содержится в HttpClientImpl.java:

// Main loop for this client's selector
private final static class SelectorManager extends Thread {

Итог: я предполагаю, что где практично подразумевает, что это зависит от реализации и что нет гарантии, что предоставленный executor будет использоваться.

ПРИМЕЧАНИЕ: это отличается от исполнителя по умолчанию, где сборщик не предоставляет файл executor. В этом случае код явно создает новый пул кэшированных потоков. Другими словами, если строитель предоставляет executor, выполняется проверка личности для SelectorManager.

person Michael Easter    schedule 20.08.2018
comment
Спасибо за Ваш ответ. Это действительно похоже на деталь реализации. Что касается вашего последнего примечания, однако, когда я не указываю исполнителя в настройке клиента, зависимые действия по-прежнему используют общий пул, а не исполнитель кэша потоков по умолчанию за кулисами. - person M A; 23.08.2018