Hacker News new | ask | show | jobs
by zmmmmm 994 days ago
I think virtual threads are huge.

The problem with regular threads is (a) multi-kb memory stack per thread and (b) consuming a file handle.

Either of those severely limits the scalability of the most "natural" parallelism constructs in Java (perhaps generally). Whole classes of application can now just be built "naturally" where previously there were whole libraries to support it (actors, rxJava, etc etc).

It make take a while for people to change their habits, but this could be quite pervasive in how it changes programming in general in all JVM languages.

2 comments

You could easily have a million threads if you use multi-kb stacks. Million times multi-kb means multi-gb, that's still 3-4 orders of magnitude less than big memory servers/VMs. (and 1 order of magnitude less than a normal laptop)

What do you mean by using a file handle, is this a Windows platform thing? On *ix, threads don't use up file descriptors (but you can still have a million fd's at least on linux for other stuff if you want).

> On *ix, threads don't use up file descriptors

Thanks - this caused me to dig into the specific scenario where creating threads was exhausting file handles in my experience and you are correct - consuming a file handle is indeed not intrinsic to creating a new thread in Linux. It's insanely easy for literally anything you do with the thread to consume a file handle, but of course, that applies to virtual threads as well. Thanks!

But "multi-kb" in this context probably actually means about 1MB.
What do you base this on? The stacks and kernel bookkeeping shouldn't use nearly this much at least on linux. Keep in mind that thread stacks have are lazily allocated virtual memory so won't use as much physical memory as the thread stack size setting shows.

If these threads are handling TCP connections and L7 protocol processing on top, you're going to have nontrivial both kernel and userspace memory usage per connection too that may dwarf the thread overhead.

Here's a linux kernel dev (Ingo Molnar) benchmarking Linux in 2002 and starting just shy of 400k threads in 4 GB: https://lkml.iu.edu/hypermail/linux/kernel/0209.2/1153.html - though on a 32 bit systems lots of objects things are 50% the size compared to current 64 bit. But still gives you a ballpark.

> Either of those severely limits the scalability

you can avoid both issues by using 20yo executorservice.

If the code is simple, blocking code, then the number of threads required in the pool is the average total duration of a request times the fanout times the request rate. That number can easily reach many thousands and more.
yes, you shouldn't add blocking code into executorservice..
Wtf, where on Earth do you put blocking code then? Firing off some long-running task in a background thread through executors is bog-standard usecase.
discussion was about specific context: avoiding overhead from spawning millions of threads, in this case you shouldn't have any blocking code at all, all API should utilize epoll underneath or something similar.
And where do you handle callbacks?
Then you either don't get the same scalability that virtual threads give you or you get it but with asynchronous code that requires not just more work but can't enjoy the same observability/debuggability on the Java platform.
could you give example what requires more work exactly and where virtual threads give more "observability"?..
Sure. Because handling server requests typically requires IO, if you wish not to block you need some way to sequence operations that is different from the ordinary sequential composition of the language (beforeIO(); blockingIO(); afterIO()). Similarly, other language constructs that build on top of basic sequential composition -- loops, exceptions, try/finally -- no longer work across the IO boundary. Instead you must reach for an asynchronous composition DSL (such as the one offered by CompletableFuture) which is not as composable as the basic language primitives.

Moreover, the platform now has no insight about your composition. Exceptions, which are designed to give context in the form of a thread stack trace, simply don't know about the context as it's not composed through the normal composition (in plain terms, stack traces in asynchronous code don't give you the operation's context). Debuggers cannot step through the asynchronous flow because the platform's built in debugging support works only by stepping through threads, and profilers are no longer able to assign IO to operations: a server that's under heavy load may show up as idle thread pools in a profiler because the platform cannot assign an asynchronous operation to some asynchronous pipeline such as CompletableFutures because these are not observable constructs of the Java platform.

Virtual threads give you the same scalability as asynchronous code but in a way that fits with the design of the Java platform. All language constructs work and compose well, debuggers step through code, and profilers can understand what's going on.

That's not to say that some other platform could not be designed around a different construct, but the Java platform -- from language, through libraries and bytecode, and all the way to the VM and its tooling interfaces -- was designed around the idea that sequential composition occurs by sequencing operations on a single thread. And virtual threads are just Java threads.