15

I was investigating how Project Loom works and what kind of benefits it can bring to my company.

So I understand the motivation, for standard servlet based backend, there is always a thread pool that executes a business logic, once thread is blocked because of IO it can't do anything but wait. So let's say I have a backend application that has single endpoint , the business logic behind this endpoint is to read some data using JDBC which internally uses InputStream which again will use blocking system call( read() in terms of Linux). So if I have 200 hundred users reaching this endpoint, I need to create 200 threads each waiting for IO.

Now let's say I switched a thread pool to use virtual threads instead. According to Ben Evans in the article Going inside Java’s Project Loom and virtual threads:

Instead, virtual threads automatically give up (or yield) their carrier thread when a blocking call (such as I/O) is made.

So as far as I understand, if I have amount of OS threads equals to amount of CPU cores and unbounded amount of virtual threads, all OS threads will still wait for IO and Executor service won't be able to assign new work for Virtual threads because there are no available threads to execute it. How is it different from regular threads , at least for OS threads I can scale it to thousand to increase the throughput. Or Did I just misunderstood the use case for Loom ? Thanks in advance

Addon

I just read this mailing list:

Virtual threads love blocking I/O. If the thread needs to block in say a Socket read then this releases the underlying kernel thread to do other work

I am not sure I understand it, there is no way for OS to release the thread if it does a blocking call such as read, for these purposes kernel has non blocking syscalls such as epoll which doesn't block the thread and immediately returns a list of file descriptors that have some data available. Does the quote above implies that under the hood , JVM will replace a blocking read with non blocking epoll if thread that called it is virtual ?

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
Almas Abdrazak
  • 3,209
  • 5
  • 36
  • 80

3 Answers3

17

Your first excerpt is missing the important point:

Instead, virtual threads automatically give up (or yield) their carrier thread when a blocking call (such as I/O) is made. This is handled by the library and runtime [...]

The implication is this: if your code makes a blocking call into the library (for example NIO) the library detects that you call it from a virtual thread and will turn the blocking call into a non-blocking call, park the virtual thread and continue processing some other virtual threads code.

Only if no virtual thread is ready to execute will a native thread be parked.

Note that your code never calls a blocking syscall, it calls into the java libraries (that currently execute the blocking syscall). Project Loom replaces the layers between your code and the blocking syscall and can therefore do anything it wants - as long as the result for your calling code looks the same.

Thomas Kläger
  • 17,754
  • 3
  • 23
  • 34
  • 2
    So if I want to read from socket using virtual thread and use InputStream for that, JVM runtime will notice it and instead of using syscall read() in Linux that blocks OS thread, it will replace it with something like epoll() add a callback and would unpark the virtual thread later on to execute the callback. Am I right ? – Almas Abdrazak Nov 30 '21 at 20:52
  • I posted my own answer, still thanks for your explanation – Almas Abdrazak Nov 30 '21 at 21:40
9

I finally found an answer . So as I said , by default InputStream.read method makes a read() syscall which according to Linux man pages will block the underling OS thread. So how is it possible that Loom won't block it ? I found an article that shows the stacktrace So if this block of code will be executed by virtual thread

URLData getURL(URL url) throws IOException {
  try (InputStream in = url.openStream()) {//blocking call
    return new URLData(url, in.readAllBytes());
  }
}

JVM runtime will transform it into the following stacktrace

java.base/jdk.internal.misc.VirtualThreads.park(VirtualThreads.java:60)//this line parks the virtual thread
java.base/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:184)
java.base/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:212)
java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:356)//JVM runtime will replace an actual read() into read from java nio package 
java.base/java.io.InputStream.readAllBytes(InputStream.java:346)

How JVM knows when to unpark the virtual thread ? Here is the stacktrace that will be ran once readAllBytes is finished

"Read-Poller" #16
  java.base@17-internal/sun.nio.ch.KQueue.poll(Native Method)
  java.base@17-internal/sun.nio.ch.KQueuePoller.poll(KQueuePoller.java:65)
  java.base@17-internal/sun.nio.ch.Poller.poll(Poller.java:195)

The author of the article uses MacOs, Mac uses kqueue as non blocking syscall, If I run it on Linux, I would see epoll syscall.

So basically Loom doesn't introduce anything new, under the hood it's a plain epoll syscall with callbacks which can be implelemented using a framework such as Vert.x that uses Netty under the hood, but in Loom the callback logic is encapsulated with the JVM runtime which I found counter intuitive, when I call InputStream.read() I do expect a corresponding read() syscall, but JVM will replace it with non blocking syscalls.

Almas Abdrazak
  • 3,209
  • 5
  • 36
  • 80
  • 7
    _when I call InputStream.read() I do expect a corresponding read() syscall_: why do you expect that? Because the current implementation does it? In the end for your code it's not important that the corresponding read() syscall is executed, it is important that upon return from `InputStream.read()` the data has been read. Everything else should be an implementation detail. – Thomas Kläger Nov 30 '21 at 22:01
  • 3
    I personally think it's important to know which syscall is executed , it allows me to understand how IO package works in java and in case of degradation performance I can see syscalls from my production machine and check which process performing badly and why. – Almas Abdrazak Nov 30 '21 at 22:18
  • @ThomasKläger just to give you an example. consider this case https://gist.github.com/strogiyotec/b156e24137f5a1fcdf0260fb45a21b8f Java io package in uninterruptible, why ? Because it blocks OS thread on read() syscall, I had this problem at work and in order to understand why I had to understand what's going on in terms of OS calls, I don't believe one can fix these type of issues without understanding what is going on under the hood and just blindly believe that "that upon return from InputStream.read() the data has been read" cheers – Almas Abdrazak Nov 30 '21 at 22:40
  • 3
    An `InputStream` is not always a `FileInputStream`. There’s `ByteArrayInputStream` and even when actually reading from a file, the `FileInputStream` might be wrapped in a `BufferedInputStream` but it could also be a `ZipInputStream` wrapping a `FileInputStream` and still, not every `read` call ends up in a system call. And even if it does end up in a system call, there never was a specification saying that the system call had to have the same name as this method. It’s ok if you think, you have to know what’s going on, but the implementation has no obligation to follow your expectation. – Holger Dec 01 '21 at 10:10
  • @Holger you are right, jdk doesn't explicitly say which syscall is used because it's also OS dependent and because it can be easily changed by oracle – Almas Abdrazak Dec 01 '21 at 16:31
2

The Answer by Thomas Kläger is correct. I’ll add a few thoughts.

So as far as I understand, if I have amount of OS threads equals to amount of CPU cores and unbounded amount of virtual threads, all OS threads will still wait for IO

No, incorrect, you misunderstand.

What you describe is what happens under current threading technology in Java. With a one-to-one mapping of Java thread to host OS thread, any call made in Java that blocks (waiting a relatively long time for a response) leaves that host thread twiddling its thumbs, doing no work. This would not be a problem if the host had a zillion threads so that other threads could be scheduled for work on a CPU core. But host OS threads are quite expensive, so we do not have a zillion, we have very few.

Using Project Loom technology, the JVM detects the blocking call, such as waiting for I/O. Once detected, the JVM sets aside (“parks”) the virtual thread as it waits for I/O response. The JVM assigns a different virtual thread to that host OS carrier thread, so that “real” thread may continue performing work rather than waiting while twiddling its thumbs. Since the virtual threads living within the JVM are so cheap (highly efficient with both memory and CPU), we can have thousands, even millions, for the JVM to juggle.

In your example of 200 threads each waiting for IO response form JDBC calls to a database, if those were virtual threads that would all be parked within the JVM. The few host OS threads used as carrier threads by your ExecutorService will be working on other virtual breads that are not currently blocked. This parking and rescheduling of blocked-then-unblocked virtual threads is handled automatically by the Project Loom technology within the JVM, with no intervention needed by us Java app developers.

let's say I switched a thread pool to use virtual threads

Actually, there is no pool of virtual threads. Each virtual thread is fresh and new, with no recycling. This eliminates worrying about thread-local contamination.

ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor() ;
…
executorService.submit( someTask ) ;  // Every task submitted gets assigned to a fresh new virtual thread.

To learn more, I highly recommend viewing the videos of presentations and interviews by Ron Pressler or Alan Bateman, members of the Project Loom team. Find the most recent, as Loom has been evolving.

And read the new Java JEP, JEP draft: Virtual Threads (Preview).

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • Thanks for the response. This part when JVM detects blocking IO and parks virtual thread rather than OS thread. How is it possible ? Linux read() syscalls blocks OS thread, if virtual thread executes the blocking syscall it will block the OS thread, is my assumption correct , JVM will replace all read() calls with epoll() so OS thread won't be blocked ? – Almas Abdrazak Nov 30 '21 at 20:49
  • @AlmasAbdrazak Those technical details I do not know. Have you studied the JEP yet? Also, the Loom team might welcome a query for such details, as they are now seeking input and feedback from the public. And their source code is open-source, with implementations [available now](https://jdk.java.net/loom/) for various platforms including Windows on x64, and both Linux & macOS on both x64 & AArch64. – Basil Bourque Nov 30 '21 at 20:58
  • I read the JEP, it's still unclear for me how virtual threads do not block OS thread during blocking syscalls As far as I understood Input/Output streams classes were changed to remove synchornize blocks. Also from the JEP "When a virtual thread tries to park, say, by performing a blocking I/O operation, while pinned, rather than released, its underlying OS thread will be blocked for the duration of the operation." Pinned here means VM can't suspend it. So I think reading from sockets using InputStream or using JDBC to interact with database would still block os thread – Almas Abdrazak Nov 30 '21 at 21:06
  • I will read the source code to better understand what's going on under the hood, thanks – Almas Abdrazak Nov 30 '21 at 21:07