Lightweight threads can’t allow for arbitrary control of the continuation directly, so there are “libraries” that you can build on coroutines that you can’t build on Loom, no matter how hard you try; it’s a fundamental limitation of the fact that they’re bundled as executable tasks rather than as open suspendables. It’s the same kind of issue as internal vs. external iteration, a.k.a. why you can’t implement Sequence.asIterator
purely in terms of Sequence.forEach
without the overhead of an intermediate data structure.
Thanks for the link – it’s super interesting. The transition from “fibers”, with their own API, to “virtual threads” that are managed with the existing Thread
API, is really nice.
I’ve been doing multithreaded programming for a long time, though, and I think a lot of people are forgetting how difficult it is, and failing to appreciate how much more difficult it will be when you have millions of threads.
I still think that Kotlin coroutines built on virtual threads will be a much nicer way to code… unless JetBrains messes it up
All of those things can be implemented using virtual threads with special schedulers – which let you control the continuation directly.
From “State of Loom, Part 1”:
Virtual threads are preemptive, not cooperative — they do not have an explicit
await
operation at scheduling (task-switching) points. Rather, they are preempted when they block on I/O or synchronization.
This means they can’t be used for things like delimited control or builders. When I said “arbitrary”, I meant “arbitrary”.
That doesn’t follow.
How not?
Well, it just doesn’t. In what way do you think delimited control or builders require protection from preemption? There are things that are kinda like that would be required to exactly mimic Kotlin behaviour, but like Kotlin’s other structured concurrency mechanisms, those can be implemented with special schedulers as I said. Using a virtual thread bound to a special scheduler, you can implement Kotlin’s native Continuations.
Also I noticed that project Loom has a Continuation
class, which they said may become public API at some point. (It’s one shot, delimited, multi-prompt - and I know what the first 2 of those mean )
Prove to me that you can implement a sequence builder like the one in Kotlin’s standard library in pure Java using the facilities of Loom. Lack of lambdas-with-receiver and trailing lambda syntax aside, you should be able to achieve basically the same API surface. To make it concrete, I should be able to write the following:
final Stream<String> fizzBuzz = SequenceBuilder.<String>build(cxt -> {
for (int x = 0;; x++) {
boolean is3 = x % 3 == 0;
boolean is5 = x % 5 == 0;
if (is3 && is5) cxt.yield(“FizzBuzz”);
else if (is3) cxt.yield(“Fizz”);
else if (is5) cxt.yield(“Buzz”);
else cxt.yield(“” + x);
}
}).asStream();
and have it produce a fully concurrency-safe lazy infinite stream.
I’m not sure what mtimmerm had in mind, but I think it depends what API they give us. I’m not sure they know yet what all the API will be. If the Continuation
is public, then it’s possible. Also, part 2 of State of Loom had an example with “channels”. I think that could do it. Kotlin-ish pseudocode…
val channel = Channel()
Thread.startVirtualThread {
var n = 0
while (true) {
channel.send(n)
n += 1
}
}
val iter = Iterator {
fun next(): String { channel.receive() }
}
for (n in iter) { ... }
Okay, and what if I have print statements in the builder closure? Will those be executed lazily, or will the virtual thread forge ahead and use the channel as a buffer, thus defeating the purpose?
With the channels I’ve seen (like Clojure’s core.async) that is configurable. You set the buffer size to zero and it would do what you want.
I’ve also seen videos on youtube about Loom where they were talking about generators, which are exactly the API you’re describing. One video was from 2018, so I don’t know if they have Generators in the early access API yet, but it sounds like they intend to provide stuff like that.
That’s fair; I didn’t know they intended to provide generators too.
That still doesn’t cover some other things that full exposure of delimited continuations can have. Even the fact that a Continuation
class is exposed isn’t enough because there’s no way to reify it at an arbitrary point. Basically I’m asking for the ability to use shift
and reset
(by whatever name), which you can do (within a controlled scope) in Kotlin.
Proving things to you doesn’t sound like a good use of my time. But the basic strategy here is that the sequence builder runs the lambda with a special scheduler so that:
-
yield
stores the output and sets a flag to indicate that it is yielding, and thenpark
s the virtual thread. - At that point, control returns to the scheduler that is running it, which returns the value out to the stream.
- When the stream pulls another value, the virtual thread is unparked.
- The unpark operation sends the vthread’s
VirtualThreadTask
to the scheduler for execution - The scheduler runs it until it blocks again.
The scheduler that runs the virtual thread can distinguish between a block due to yield and a blocking IO call, because in the latter case, the “yield flag” isn’t set. When another kind of block occurs, the scheduler blocks its own thread until it’s called back by the system to continue the virtual thread execution.
There is a discussion about this article in a kotlin slack. I think that the general description is good, but that the author cofuses asynchronous program model with threading, which leads him to not quite correct conclussions.
Maybe interesting in this context is that Brian Goetz thinks that project Loom will make reactive programming obsolete.
I still think that couroutines might not be beneficial compared to the simpler and less invasive virtual threads in many scenarios. It looks like Pivotal, the company behind Spring, has the same opinion: Embracing Virtual Threads
I don’t think anyone is saying that virtual threads are a bad idea, just that coroutines as they are implemented in Kotlin are much more than just “cheap concurrency”.
You can’t do this with virtual threads:
// Cheap, lazy, synchronous iterator
val fibonacci = sequence {
yield(1)
var cur = 1
var next = 1
while (true) {
yield(next)
val tmp = cur + next
cur = next
next = tmp
}
}
fun main(args: Array<String>) {
println(fibonacci.take(10).joinToString())
}
Coroutines also make Arrow’s bind operator possible.
Virtual threads do not have a concept of hierarchy, which is extremely useful (on the client side: multiple requests originate from a component in React or Compose, when the component goes out of the screen all requests related to it are automatically cancelled; on the server side: multiple sub-tasks can be started for a single client request and if any of them fail you’re guaranteed the others are killed). It allows the entire codebase to use asynchronous methods without breaking the application’s lifecycle.
And most importantly, they’re multiplatform. JavaScript doesn’t have threads (virtual or otherwise) but still needs concurrency, Native doesn’t have virtual threads, etc.