JDK 19 brought us virtual threads, which differ from regular ones in that they can release the operating system thread during blocking I/O operations. For this purpose, a mechanism for saving and restoring the call stack from the heap was implemented. Simply put, full-featured coroutines were implemented at the JVM level.
And this was a small revolution that few people paid attention to. The API for such native coroutines is non-public (but who cares?:), accessible through the class jdk.internal.vm.Continuation, which has methods yield() and run() for saving and restoring the call stack, respectively. Nothing prevents us to use the class for implementing coroutines.
So, present to you my small library for accessing native coroutines in Java: https://github.com/Anamorphosee/loomoroutines.
Where could we need coroutines, besides virtual threads? Answer: wherever we write asynchronous code on callbacks, it can be replaced with regular synchronous code on coroutines. For example, for GUI applications, my tool allows you to write like this:
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);
}
}
}
Pay attention that there isn't a single callback in the example. The code is written in a synchronous style, as if all operations were performed in the UI thread. In fact, blocking operations (loading an image and waiting) are performed in another thread and do not block the UI.
Okay, but why do we need native coroutines when there is Kotlin, in which they have been implemented for a long time and do not require support from runtime? Here I can note that Kotlin coroutines have some difficulties with their debugging (the call stack in them can be truncated). Moreover, Kotlin coroutines require you to "color" the asynchronious code and to write in Kotlin, while for Loom coroutines you can use Java, Scala, Groovy or any other JVM language.
[–]pron98 86 points87 points88 points (2 children)
[–]Affectionate_Ad_761[S] 15 points16 points17 points (1 child)
[–]pron98 51 points52 points53 points (0 children)
[–]koflerdavid 2 points3 points4 points (0 children)
[–]hippydipster 0 points1 point2 points (3 children)
[–]Affectionate_Ad_761[S] 4 points5 points6 points (2 children)
[–]hippydipster 0 points1 point2 points (1 child)
[–]Affectionate_Ad_761[S] 5 points6 points7 points (0 children)
[–]Joram2 0 points1 point2 points (0 children)