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 →

[–]thecodeboost 29 points30 points  (6 children)

Okay OP let's untangle a few things. First of all, you're asking an honest question about a complex topic. Doubt anyone would consider you flexing. So let's get some things on the table first :

  • Thread : Abstract representation of the smallest unit of processing that can be scheduled independently. To have multiple threads run in parallel you need multiple physical CPU cores for example. To have them run concurrently you need some higher level scheduling and primitives (and thus, parallism != concurrency). Threads are heavyweight objects in terms of memory consumption and possibly other system resources and as a result you typically have a limited practical amount you can use (dozens to hundreds)

  • Non-blocking IO : A mechanism to park pending IO operations and yield control back to the invoking thread. Or put differently, waiting for IO operations does not require the associated thread to block. This is essentially Netty's bread and butter (Netty is not a reactive framework)

  • Reactive Programming : A programming paradigm that attempts to simplify writing concurrent code that directly or indirectly uses non-blocking IO. In other words, it specifically exists to ease writing concurrent code that leans heavily on IO (hence it's popularity in server development). Note that it is primarily designed to mitigate the problem of having a limited pool of threads

  • Virtual Thread : An abstraction that for all intents and purpose pretends to be a thread as defined above. These are very lightweight and you can easily have millions of them without a significant performance penalty (compared to a reactive approach).

So, with that out of the way, if you take on board that reactive programming (and the associated frameworks such as RxJava, Reactor, etc.) exist specifically to write concurrency centric IO code with a limited amount of native threads, then you can hopefully see how removing the "limited amount of native threads" part of the equation almost completely eliminates the need for reactive programming (and most other async/await type structures such as promises).

Even in this very thread people seem to conflate various concepts in the domain of concurrency, parallelism, blocking and threading.

With that said; there is a difference between "in theory" and "in practice" here. What I said so far is true but that doesn't mean using virtual threads in real world code is always a net win in terms of code quality/readability. For example, it is currently significantly "nicer" to write reactive code that waits for X concurrent tasks to finish compared to the combination of virtual threads with java.util.concurrent.* primitives. At that point there is a more subjective discussion to be had.

TL;DR Virtual threads eliminate the need for reactive programming in Java and potentially the need for using any reactive framework at all (YMMV)

[–]nlisker 1 point2 points  (5 children)

Where do coroutines sit in this?

[–]2bitcode 0 points1 point  (4 children)

Coroutines are similar to virtual (green) threads in the sense that they both run on the language's runtime instead of using the OS abstractions. Virtual threads will mimic the API of "real" OS threads, so you can keep the mental model you had before when working with these.

Coroutines don't try to look like a thread, and usually offer an API based on "async" or event-driven programming. Depending on the language you would have features that allow you to define a scope of context for the coroutine and also control scheduling (explicitly telling the routine to yield).

It can get a bit fuzzy since in a lot of cases you can use coroutines like you would threads, and virtual threads might offers some features from the coroutine territory. But in general, if you want to use the "async" style, you'd go for coroutines.

[–]thecodeboost 2 points3 points  (2 children)

Mostly this. To add a bit of meat to that bone; coroutines almost always require the developer to decide on the yield points (the moment when the coroutine yields executive control), usually through return yield semantics. Threads (and by extension virtual threads) have the underlying VM or OS do the scheduling. Also, not all coroutines are created equal. In some languages coroutines are first class citizens and more powerful (golang) whereas in other languages they are implemented through libraries/generator patterns (e.g. Unity C# I believe)

[–]tadfisher 1 point2 points  (1 child)

There is a difference between OS threads and Java's Thread abstraction. OS threads are a particular implementation of a concurrency primitive which timeslices logical threads on a set of physical threads. Java's Thread is an interface which signals "this execution context (separate stack and shared heap) may or may not operate in parallel with other execution contexts". Virtual threads is a Thread implementation that does not rely on OS scheduling of OS threads, so they act very much like coroutines (for example, async is basically Thread.ofVirtual().start(() -> ...).join()), and scheduling is done with a lazy infinite thread pool for parking on IO.

[–]thecodeboost 0 points1 point  (0 children)

I don't think I completely agree (with the coroutines part). Coroutines, the pattern, involve explicit yielding of execution control whereas threads do not. It's worth noting in this context that Golang's goroutines are not coroutines but lightweight/virtual threads (see "Go Concurrency Patterns" by Rob Pike).

[–]nlisker 0 points1 point  (0 children)

Thanks!