Будут ли исключения в Project Loom когда-нибудь распространяться через контексты ExecutorService?
Из ткацкой лаборатории , учитывая код
var virtualThreadFactory = Thread.ofVirtual().factory();
try (var executorService = Executors.newThreadPerTaskExecutor(virtualThreadFactory)) {
IntStream.range(0, 15).forEach(item -> {
executorService.submit(() -> {
try {
var milliseconds = item * 1000;
System.out.println(Thread.currentThread() + " sleeping " + milliseconds + " milliseconds");
Thread.sleep(milliseconds);
System.out.println(Thread.currentThread() + " awake");
if (item == 8) throw new RuntimeException("task 8 is acting up");
} catch (InterruptedException e) {
System.out.println("Interrupted task = " + item + ", Thread ID = " + Thread.currentThread());
}
});
});
}
catch (RuntimeException e) {
System.err.println(e.getMessage());
}
Я надеялся, что код будет
catch
в
RuntimeException
и распечатайте сообщение, но это не так.
Я слишком на многое надеюсь, или когда-нибудь это сработает, как я надеюсь?
В ответ на удивительный ответ Стивена Си, который я полностью ценю, при дальнейшем исследовании я обнаружил через
static String spawn(
ExecutorService executorService,
Callable<String> callable,
Consumer<Future<String>> consumer
) throws Exception {
try {
var result = executorService.submit(callable);
consumer.accept(result);
return result.get(3, TimeUnit.SECONDS);
}
catch (TimeoutException e) {
// The timeout expired...
return callable.call() + " - TimeoutException";
}
catch (ExecutionException e) {
// Why doesn't malcontent get caught here?
return callable.call() + " - ExecutionException";
}
catch (CancellationException e) { // future.cancel(false);
// Exception was thrown
return callable.call() + " - CancellationException";
}
catch (InterruptedException e) { // future.cancel(true);
return callable.call() + "- InterruptedException ";
}
}
а также
try (var executorService = Executors.newThreadPerTaskExecutor(threadFactory)) {
Callable<String> malcontent = () -> {
Thread.sleep(Duration.ofSeconds(2));
throw new IllegalStateException("malcontent acting up");
};
System.out.println("\n\nresult = " + spawn(executorService, malcontent, (future) -> {}));
} catch (Exception e) {
e.printStackTrace(); // malcontent gets caught here
}
я ожидал
malcontent
попасть в ловушку
spawn
как
ExecutionException
согласно документации, но это не так. Следовательно, мне трудно рассуждать о своих ожиданиях.
Большая часть моей надежды на Project Loom заключалась в том, что, в отличие от функционального реактивного программирования, я мог снова полагаться на исключения, чтобы делать правильные вещи, и рассуждать о них так, чтобы я мог предсказать, что произойдет, без необходимости проводить эксперименты, чтобы проверить, что на самом деле происходит. .
Как говорил Стив Джобс (из NeXT): «Это просто работает»
Пока что на мою публикацию на loom-dev@openjdk.java.net не ответили ... вот почему я использовал StackOverflow. Я не знаю, как лучше всего привлечь разработчиков Project Loom.
3 ответа
Это предположение ... но я так не думаю.
Согласно предварительным документам javadoc , теперь наследует
AutoClosable
, и указано, что поведение метода по умолчанию - выполнить полное завершение работы и дождаться его завершения. (Обратите внимание, что это описывается как поведение по умолчанию, а не обязательное поведение!)
Так почему же они не могли изменить поведение, чтобы отловить исключения в стеке этого потока?
Одна из проблем заключается в том, что определение шаблонов поведения, логически согласованных как для этого случая, так и для случая, когда объект не используется в качестве ресурса в
try with resources
. Чтобы реализовать поведение в этом случае, метод должен быть проинформирован какой-либо другой частью службы исполнителя о необработанном исключении задачи. Но если ничего не вызывает, то исключения не могут быть повторно вызваны. И если
close()
вызывается в финализаторе или подобном, вероятно, не будет ничего, что могло бы с ними справиться. По крайней мере, это сложно.
Вторая проблема заключается в том, что в общем случае было бы трудно обработать исключение (я). Что делать, если не удалось выполнить несколько задач из-за исключения? Что делать, если разные задачи терпят неудачу с разными исключениями? Как код, обрабатывающий исключение (например, ваш
catch (RuntimeException e) ...
выяснить, какая задача не удалась?
Третья проблема заключается в том, что это будет серьезное изменение. В Java 17 и более ранних версиях приведенный выше код не распространяет исключения из задач. В Java 18 и более поздних версиях это было бы. Код Java 17, предполагавший отсутствие «случайных» исключений из неудавшихся задач, доставленных в этот поток, сломается.
Четвертый момент заключается в том, что это будет неприятностью в случаях использования, когда программист Java 18+ хочет рассматривать службу исполнителя как ресурс, но не хочет иметь дело с «случайными» исключениями в этом потоке. (Я подозреваю, что это будет большинство случаев использования для автоматического закрытия службы исполнителя.)
Пятая проблема (если вы хотите это так называть) заключается в том, что это критическое изменение для первых последователей Loom. (Я читаю ваш вопрос как о том, что вы пробовали это с помощью Loom, и в настоящее время он ведет себя не так, как вы предлагали.)
Последняя проблема заключается в том, что уже есть способы зафиксировать исключение задачи и доставить его; например, через
CompletableFuture
объекты. Это предложение не заполняет пробел в
ExecutorService
функциональность.
(Уф!)
Конечно, я не знаю, что разработчики Java действительно сделают. И мы не узнаем все вместе, пока Loom, наконец, не будет выпущен как функция без предварительной версии основной Java.
В любом случае, если вы хотите лоббировать это, вы должны написать об этом по электронной почте в список рассылки Loom.
LOOM внес много улучшений, таких как
AutoClosable
поэтому он упрощает кодирование, устраняя вызовы
shutdown
/
awaitTermination
.
Ваша точка зрения на ожидание аккуратной обработки исключений применима к типичному использованию в любом JDK, а не только в предстоящем выпуске LOOM, поэтому очевидно, что IMO не нужно связывать с работой LOOM.
Обработку ошибок, которую вы хотите, довольно легко включить в любую версию JDK, добавив несколько строк кода вокруг блоков кода, которые используют
ExecutorService
:
var ex = new AtomicReference<RuntimeException>();
try {
// add any use of ExecutorService here
// eg OLD JDK style:
// var executorService = Executors.newFixedThreadPool(5);
try (var executorService = Executors.newThreadPerTaskExecutor(virtualThreadFactory)) {
...
if (item == 8) {
// Save exception before sending:
ex.set(new RuntimeException("task 8 is acting up"));
throw ex.get();
}
...
}
// OR: not-LOOM JDK call executorService.shutdown/awaitTermination here
// Pass on any handling problem
if (ex.get() != null)
throw ex.get();
}
catch (Exception e) {
System.err.println("Exception was: "+e.getMessage());
}
Не так элегантно, как вы надеетесь, но работает в любом выпуске JDK.
ИЗМЕНИТЬ По отредактированному вопросу:
Вы положили
callable.call()
как код внутри
catch (ExecutionException e) {
так что вы потеряли первое исключение и
malcontent
вызывает второе исключение. Добавлять
System.out.println
чтобы увидеть оригинал:
catch (ExecutionException e) {
System.out.println(Thread.currentThread()+" ExecutionException: "+e);
e.printStackTrace();
// Why doesn't malcontent get caught here?
return callable.call() + " - ExecutionException";
}
Я думаю, что ближе всего к тому, чего вы пытаетесь достичь,
try(var executor = StructuredExecutor.open()) {
var handler = new StructuredExecutor.ShutdownOnFailure();
IntStream.range(0, 15).forEach(item -> {
executor.fork(() -> {
var milliseconds = item * 100;
System.out.println(Thread.currentThread()
+ "sleeping " + milliseconds + " milliseconds");
Thread.sleep(milliseconds);
System.out.println(Thread.currentThread() + " awake");
if(item == 8) {
throw new RuntimeException("task 8 is acting up");
}
return null;
}, handler);
});
executor.join();
handler.throwIfFailed();
}
catch(InterruptedException|ExecutionException ex) {
System.err.println("Caught in initiator thread");
ex.printStackTrace();
}
который будет запускать все задания в виртуальных потоках и генерировать исключение в потоке инициатора при сбое одного из заданий.
StructuredExecutor
— это новый инструмент, представленный проектом Loom, который позволяет показать принадлежность созданных виртуальных потоков к этому конкретному заданию в диагностических инструментах. Но обратите внимание, что это
close()
не будет ждать завершения, а потребует, чтобы владелец сделал это перед закрытием, выбрасывая исключение, если разработчик не смог этого сделать.
Поведение классических реализаций не изменится.
Решение для
ExecutorService
было бы
try(var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var jobs = executor.invokeAll(IntStream.range(0, 15).<Callable<?>>mapToObj(item ->
() -> {
var milliseconds = item * 100;
System.out.println(Thread.currentThread()
+ " sleeping " + milliseconds + " milliseconds");
Thread.sleep(milliseconds);
System.out.println(Thread.currentThread() + " awake");
if(item == 8) {
throw new RuntimeException("task 8 is acting up");
}
return null;
}).toList());
for(var f: jobs) f.get();
}
catch(InterruptedException|ExecutionException ex) {
System.err.println("Caught in initiator thread");
ex.printStackTrace();
}
Обратите внимание, что пока
invokeAll
ждет завершения всех заданий, нам еще нужен вызов цикла
get
обеспечить соблюдение
ExecutionException
быть брошенным в инициирующем потоке.