项目织机,当虚拟线程进行阻塞系统调用时会发生什么?

Project loom, what happens when virtual thread makes a blocking system call?

我正在研究 Project Loom 的工作原理以及它可以为我的公司带来什么样的好处。

所以我明白了动机,对于标准的基于servlet的后端,总是有一个执行业务逻辑的线程池,一旦线程因为IO而被阻塞,它只能等待。因此,假设我有一个具有单个端点的后端应用程序,该端点背后的业务逻辑是使用 JDBC 读取一些数据,它在内部使用 InputStream 再次使用阻塞系统调用( read() 在 Linux).因此,如果我有 20000 个用户到达此端点,我需要创建 200 个线程,每个线程等待 IO。

现在假设我将线程池切换为使用虚拟线程。根据 Ben Evans 在文章 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.

据我了解,如果我的 OS 线程数量等于 CPU 核心数量和无限数量的虚拟线程,所有 OS 线程仍将等待对于 IO 和 Executor 服务将无法为虚拟线程分配新工作,因为没有可用的线程来执行它。它与常规线程有何不同,至少对于 OS 个线程,我可以将其扩展到千个以提高吞吐量。还是我只是误解了 Loom 的用例?提前致谢

插件

我刚刚读了这个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

我不确定我是否理解它,OS 没有办法释放线程,如果它执行诸如读取之类的阻塞调用,出于这些目的,内核具有非阻塞系统调用,例如 epoll,它不't 阻塞线程并立即 returns 具有一些可用数据的文件描述符列表。上面的引述是否暗示在幕后,如果调用它的线程是虚拟的,JVM 会将阻塞 read 替换为非阻塞 epoll

您的第一个摘录遗漏了要点:

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 [...]

含义是这样的:如果您的代码对库(例如 NIO)进行阻塞调用,库检测到您从虚拟线程调用它,并将阻塞调用转换为非阻塞调用,停放虚拟线程并继续处理一些其他虚拟线程代码。

只有当没有虚拟线程准备好执行时,才会停放本机线程。

请注意,您的代码从不 调用阻塞系统调用,它调用java 库(当前执行阻塞系统调用)。 Project Loom 替换了您的代码和阻塞系统调用之间的层,因此可以做任何它想做的事情——只要您的调用代码的结果看起来相同。

是正确的。我会补充一些想法。

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

不对,你误会了。

您所描述的是Java中当前线程技术下发生的情况。通过 Java 线程到主机 OS 线程的一对一映射,在 Java 中进行的任何阻塞调用(等待相对较长的响应时间)都会使主机线程旋转它的拇指,没有做任何工作。如果主机有不计其数的线程,那么其他线程可以安排在 CPU 核心上工作,这将不是问题。但是主机 OS 线程非常昂贵,所以我们没有很多,我们只有很少的。

使用Project Loom技术,JVM检测阻塞调用,比如等待I/O。一旦检测到,JVM 会在等待 I/O 响应时搁置(“停放”)虚拟线程。 JVM 为该主机 OS 载体线程分配一个不同的虚拟线程,以便“真实”线程可以继续执行工作,而不是在等待时摆弄拇指。由于 JVM 中的虚拟线程非常便宜(内存和 CPU 都非常高效),我们可以有数千甚至数百万的 JVM 来处理。

在您的 200 个线程的示例中,每个线程都在等待 IO 响应表单 JDBC 对数据库的调用,如果这些都是虚拟线程,那么它们将全部停放在 JVM 中。您的 ExecutorService 用作载体线程的少数主机 OS 线程将在当前未被阻止的其他虚拟面包上工作。这种停放和重新安排阻塞然后解除阻塞的虚拟线程是由 JVM 中的 Project Loom 技术自动处理的,不需要我们 Java 应用程序开发人员的干预。

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

实际上,没有虚拟线程池。每个虚拟线程都是新鲜的,没有回收。这消除了对线程局部污染的担忧。

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

要了解更多信息,我强烈建议您观看 Project Loom 团队成员 Ron Pressler 或 Alan Bateman 的演讲和采访视频。查找最新的,因为 Loom 一直在发展。

并阅读新的 Java JEP,JEP draft: Virtual Threads (Preview)

我终于找到了答案。所以正如我所说,默认情况下 InputStream.read 方法会进行 read() 系统调用,根据 Linux 手册页,它会阻塞底层 OS 线程。那么 Loom 怎么可能不会阻止它呢?我找到了一个显示堆栈跟踪的 article 所以如果这段代码将由虚拟线程执行

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

JVM 运行time 将 t运行sform 成下面的 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)

JVM 如何知道何时取消停放虚拟线程?这是 运行 一旦 readAllBytes 完成

的堆栈跟踪
"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)

文章作者使用MacOs,Mac使用kqueue作为非阻塞系统调用,如果我运行它在Linux,我会看到 epoll 系统调用。

所以 Loom 基本上没有引入任何新东西,在幕后它是一个普通的 epoll 带有回调的系统调用,可以使用框架实现,例如 Vert.x,在幕后使用 Netty,但在 Loom 中,回调逻辑封装在 JVM 运行time 中,我发现这有违直觉,当我调用 InputStream.read() 时,我确实希望有一个相应的 read() 系统调用,但 JVM 会将其替换为非阻止系统调用。