This is all part of the MLVM project, which aims to add first-class architectural support for languages other than Java, especially dynamic languages, to a Java Virtual Machine.
Let me show you a few examples of what you can do with Java coroutines:
Simple Coroutine
//
public class SimpleCoroutine implements Runnable {
public void run() {
System.out.println("Coroutine running 1");
Coroutine.yield();
System.out.println("Coroutine running 2");
}
public static void main(String[] args) {
Coroutine coro = new Coroutine(new SimpleCoroutine());
System.out.println("start");
Coroutine.yield();
System.out.println("middle");
Coroutine.yield();
System.out.println("end");
}
}
This is like the "Hello World" of coroutines - it jumps back and forth between the implicit thread coroutine and a coroutine that was created explicitly. The output looks like this:
start
Coroutine running 1
middle
Coroutine running 2
end
These are symmetric coroutines. Once created, they are associated with the current thread and the thread will keep track of them until they're finished. Each time yield is called the current coroutine is suspended and the next one will be resumed ("next" being defined by some reasonably fair scheduling mechanism).
You can call yieldTo to pass control to a specific coroutine, but in most cases you're probably doing something wrong if you need this.
Asymmetric Coroutines
There's also another type of coroutines that doesn't have the whole scheduling business, asymmetric coroutines. They're called asymmetric because of the way they pass control to each other - by calling and returning to each other. This means that a symmetric of asymmetric coroutine can call an asymmetric coroutine, which can then only return to its calling coroutine. It's a complicated concept, but you can do neat things with it once you know what it really does.
Just like a normal subroutines have parameters and return values, an asymmetric coroutine also has input and output values. They're generic, so that there's no need to cast all the time.
Lets look at a simple example that always returns the average of all numbers it has been given so far:
//
public class SimpleAsymCoroutine extends AsymCoroutine<Integer, Double> {
public static void main(String[] args) {
SimpleAsymCoroutine coro = new SimpleAsymCoroutine();
System.out.println(coro.call(5));
System.out.println(coro.call(20));
System.out.println(coro.call(20));
}
public Double run(Integer value) {
double sum = value;
int count = 1;
while (true) {
sum += ret(sum / count++);
}
}
}
This will output:
5.0
12.5
15.0
Nice! Both the main method and the coroutine are written as if the other part of the program was just a function that can be called, and the coroutine framework takes care of the glue between the two.
What's less nice about this example is that it directly extends the AsymCoroutine class. This can be avoided by implementing the AsymRunnable interface, which can be passed to the AsymCoroutine constructor. This is also an interesting use case for co- and contravariant generic type parameters, which is explained in more detail in the thesis.
SAX Parser Inversion
One of my favorite asymmetric coroutine examples is the SAX parser example:
//
public class CoroSaxParser extends AsymCoroutine<Void, String> {
public static void main(String[] args) {
CoroSaxParser parser = new CoroSaxParser();
while( !parser.isFinished()) {
System.out.println(parser.call());
}
}
public String run(Void value) {
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
parser.parse(new File("content.xml"), new DefaultHandler() {
public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
ret(name);
}
});
return null;
}
}
This inverts an ordinary SAX parser so that it doesn't call back for every element it finds but returns one element at a time. Due to the fact that AsymCoroutine implements Iterable it's possible to use a for-each instead of the while loop:
//
for (String element : parser) {
System.out.println(element);
}
Thread-to-Thread Migration
For performance reasons all coroutine operations always work on the current thread, and all coroutines are bound to one specific thread. This allows the coroutine system to use only a minimal amount of locking, but also means that it's illegal for a coroutine to be resumed on another thread. But sometimes this is required, e.g., for taking work from a blocked thread, and there's a simple API for this:
coro.steal();
The call can fail for a number of obvious reasons (the coroutine is running, ...) and for a number of not so obvious reasons (can't steal a thread's initial coroutine, coroutine has synchronized blocks or methods, ...).
Coroutine Serialization
A normal thread cannot be serialized: Thread is not serializable, and there's simply no way to access all the information that a thread entails, let alone a way to restore a thread in a given state.
The JVM coroutine implementation, however, provides ways to allow coroutines to be serialized. It doesn't really serialize the coroutines, it just transforms them into a Java-accessible object that can then by serialized in any way the programmer wishes.
And there's of course also a way to transform these Java-accessible objects back into a coroutine, so that it can resume execution.
There's a method called serialize that returns an array of CoroutineFrame objects, and there's a method called deserialize that replaces a new coroutine's contents with the given array of CoroutineFrames.
It's actually more complicated than that; again, the details are explained in the thesis.