Pull to refresh

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

Level of difficultyHard
Reading time3 min
Views6.9K

Начиная с 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-язык.

Tags:
Hubs:
Total votes 22: ↑17 and ↓5+16
Comments20

Articles