Будут ли исключения в 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быть брошенным в инициирующем потоке.

Другие вопросы по тегам