Асинхронное выполнение 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
.
Это ошибка в реализации? Или что-то мне не хватает? Я замечаю, что он говорит: "экземпляры выполняются в потоках, предоставленных клиентом Executor, где это практично ", так ли это случай, когда это не применяется?
Обратите внимание, что я также попробовал thenAcceptAsync
и это тот же результат.
Ответы
Ответ 1
Я только что нашел обновленную документацию (та, с которой я изначально был связан, кажется старым), где объясняет это поведение реализации:
В общем случае асинхронные задачи выполняются либо в потоке, вызывающем операцию, например, при отправке HTTP-запроса, так и в потоках, предоставленных исполнителем клиента. Зависимые задачи, вызванные возвращаемыми CompletionStages или CompletyFutures, которые явно не указывают исполнителя, выполняются в том же исполнителе по умолчанию, что и для CompletableFuture
, или вызывающий поток, если операция завершается до регистрации зависимой задачи.
И стандартный исполнитель CompletableFuture
является общим пулом.
Я также нашел идентификатор ошибки, который вводит это поведение, в котором разработчики API полностью объясняют это:
2) Зависимые задачи, выполняемые в общем пуле Выполнение зависимых задач по умолчанию было обновлено для выполнения в том же самом исполнителе, что и для CompletableFuture defaultExecutor. Это более знакомо разработчикам, которые уже используют CF, и уменьшает вероятность того, что HTTP-клиент будет голоден от потоков для выполнения своих задач. Это просто поведение по умолчанию, и HTTP-клиент, и CompletableFuture позволяют при необходимости более мелкозернистое управление.
Ответ 2
Краткая версия. Я думаю, что вы определили детали реализации и что "где это практично" означает, что нет гарантии, что предоставленный 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 selector
private final static class SelectorManager extends Thread {
Результат: я предполагаю, что практическое означает, что он зависит от реализации и что нет гарантии, что предоставленный executor
будет использоваться.
ПРИМЕЧАНИЕ. Это отличается от исполнителя по умолчанию, когда строитель не предоставляет executor
. В этом случае код явно создает новый пул кэшированных потоков. Иными словами, если строитель предоставляет executor
, выполняется проверка личности для SelectorManager
.