Project Loom. Не только виртуальные потоки

от автора

Начиная с Java 19 нам доступны виртуальные потоки, которые отличаются от обычных, тем что умеют освобождать поток операционной системы во время блокирующих I/O операций. Для этого на уровне JVM был реализован механизм сохранения в хипе и восстановления из хипа стека вызова. Проще говоря, были реализованы полноценные корутины на уровне JVM.

И это небольшая революция, на которую мало кто обратил внимание. Само API для таких нативных корутин непубличное, доступно через класс jdk.internal.vm.Continuation, в котором есть методы yield() и run() для сохранения и восстановления стека вызова соответственно. Но получить доступ до него несложно, нужно лишь добавить пару аргументов в строку запуска JVM (либо воспользоваться инструментом, который позволяет обходить ограничения JPMS).

Поэтому представляю свою небольшую библиотеку для доступа к нативным корутинам на Java: https://github.com/Anamorphosee/loomoroutines.

У многих может возникнуть вопрос, где нам могут быть нужны корутины, кроме виртуальных потоков? Ответ: везде, где мы пишем асинхронный код на колбеках, его можно заменить на синхронный код на корутинах. Например, для GUI приложений, моя обертка позволяет написать вот так:

Java GUI App Example
import dev.reformator.loomoroutines.dispatcher.SwingDispatcher; import dev.reformator.loomoroutines.dispatcher.VirtualThreadsDispatcher;  import javax.imageio.ImageIO; import javax.swing.*;  import java.awt.*; import java.awt.image.BufferedImage; import java.io.IOException; import java.net.URI; import java.time.Duration; import java.util.regex.Pattern;  import static dev.reformator.loomoroutines.dispatcher.DispatcherUtils.*;  public class ExampleSwing {     private static int pickingCatCounter = 0;      private static final Pattern urlPattern = Pattern.compile("\"url\":\"([^\"]+)\"");      public static void main(String[] args) {         var frame = new JFrame("Cats");         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);         var panel = new JPanel();         panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));         var button = new JButton("Pick a cat");         var imagePanel = new ImagePanel();         panel.add(button);         panel.add(imagePanel);         frame.add(panel);         frame.setSize(1000, 500);         frame.setVisible(true);          button.addActionListener(e -> dispatch(SwingDispatcher.INSTANCE, () -> {             pickingCatCounter++;             if (pickingCatCounter % 2 == 0) {                 button.setText("Pick another cat");                 return null;             } else {                 button.setText("This one!");                 var cachedPickingCatCounter = pickingCatCounter;                  try {                     while (true) {                         var bufferedImage = doIn(VirtualThreadsDispatcher.INSTANCE, ExampleSwing::loadCatImage);                         if (pickingCatCounter != cachedPickingCatCounter) {                             return null;                         }                          imagePanel.setImage(bufferedImage);                         delay(Duration.ofSeconds(1));                          if (pickingCatCounter != cachedPickingCatCounter) {                             return null;                         }                     }                 } catch (Throwable ex) {                     if (pickingCatCounter == cachedPickingCatCounter) {                         ex.printStackTrace();                         pickingCatCounter++;                         button.setText("Exception: " + ex.getMessage() + ". Try again?");                     }                     return null;                 }             }         }));     }      private static BufferedImage loadCatImage() {         String url;         {             String json;             try (var stream = URI.create("https://api.thecatapi.com/v1/images/search").toURL().openStream()) {                 json = new String(stream.readAllBytes());             } catch (IOException ex) {                 throw new RuntimeException(ex);             }             var mather = urlPattern.matcher(json);             if (!mather.find()) {                 throw new RuntimeException("cat url is not found in json '" + json + "'");             }             url = mather.group(1);         }         try (var stream = URI.create(url).toURL().openStream()) {             return ImageIO.read(stream);         } catch (IOException ex) {             throw new RuntimeException(ex);         }     } }  class ImagePanel extends JPanel {     private BufferedImage image = null;      public void setImage(BufferedImage image) {         this.image = image;         repaint();     }      @Override     protected void paintComponent(Graphics g) {         super.paintComponent(g);         if (image != null) {             g.drawImage(image, 0, 0, null);         }     } }

Обратите внимание, что в примере нет ни одного колбека, код написан в синхронном стиле, как будто все операции производятся в UI потоке. На самом же деле блокирующие операции (загрузка изображение и ожидание) производятся в другом потоке и не блокируют UI.

Хорошо, но зачем нам нативные которутины, когда есть Kotlin, в котором они уже давно реализованы и не требуют поддержки со стороны рантайма? Тут я могу отметить, что Kotlin-корутины реализованы слишком оптимизировано и из-за этого имеются сложности с их отладкой (в них после восстановления обрезается стек вызова). Кроме того, Kotlin-корутины обязывают использовать Kotlin, для Loom-корутин же можно использовать Java, Scala, Kotlin, Groovy или любой другой JVM-язык.


ссылка на оригинал статьи https://habr.com/ru/articles/785334/