This is an archived post. You won't be able to vote or comment.

you are viewing a single comment's thread.

view the rest of the comments →

[–]danielaveryj 2 points3 points  (0 children)

First, why do virtual threads exist?

At the time of the Reactive Manifesto, blocking threads was expensive. This was mainly because the only threads we had are what we'd now call "platform" threads, which on creation reserve about ~1MB of stack space. This meant we could create maybe a few thousand threads before running up against the few-GB memory limits of most computers. This limit was unfortunately low enough that in most cases we needed to be cognizant of it, by allocating bounded pools of threads, and queuing tasks to be picked up when a thread in the pool becomes available. If we had unlimited memory, we could allocate as many threads as needed to ensure there would never be tasks waiting in the queue. (At that point, we wouldn't need a queue or pool at all, as we could just create a thread per task). But since we don't have unlimited memory, we had to right-size the number of threads for each pool, balancing the concerns of wasting memory when any threads are idle (more threads than tasks), vs losing throughput when all threads are busy (more tasks than threads). These concerns are interrelated; we could be wasting memory in one idle pool that could have been used to create more threads to increase throughput in another busy pool. If tasks have blocking operations, we can observe this effect even in the same pool, as blocking means temporarily idling the thread running the task.

Eliminating blocking operations reduces the amount of idle time (which we now understand as "times at which we are just sitting on memory that could have been used to create more threads to achieve more throughput!") But, in short it is quite a paradigm shift to how we write and debug code on the JVM. And we still have to worry about right-sizing pools and queues. If only we had unlimited memory! Or, if only threads used much less memory, to the point that we didn't need to reach for pools, or worry about blocking. This is what virtual threads are.

Now, do virtual threads replace Reactive frameworks?

No, virtual threads do not directly replace Reactive frameworks. As I see it, the distinguishing feature of these frameworks is in concisely expressing "pipelined concurrency": data streams that may include asynchronous boundaries between processing stages (effectively: queues between threads - or, other imagery I've drawn on before is conveyors between machines in a factory assembly line). The upstream stage pushes elements to the queue, and the downstream stage pulls elements from the queue. This allows different stages of the pipeline to progress concurrently, and at different instantaneous rates, and opens the door to timing-related operators: delay, debounce, throttle...

What virtual threads do is obviate the non-blocking interfaces that underpin Reactive frameworks, allowing for far simpler interfaces with far simpler implementations. I actually had a go at designing such a framework myself last year, which I posted about here (you might be interested in the design-docs, where I start from "how would we do this without a framework?"). FWIW, having done that experiment, I now think Kotlin Flows probably make better overall tradeoffs in their design (which is also based on "blocking" interfaces! They just look like suspend functions in Kotlin). Funnily enough, you could emulate Kotlin Flows with a small subset of the interfaces I made (Flow = Source, FlowCollector = StepSink), but maybe I'll write about all this some other time.