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

Alm*_*zak 18 java concurrency multithreading project-loom

我正在研究Project Loom 的运作方式以及它能为我的公司带来什么样的好处。

\n

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

\n

现在假设我将线程池切换为使用虚拟线程。根据 Ben Evans 在《深入 Java\xe2\x80\x99s Project Loom 和虚拟线程》一文中的说法:

\n
\n

相反,当进行阻塞调用(例如 I/O)时,虚拟线程会自动放弃(或让出)其承载线程。

\n
\n

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

\n

添加在

\n

我刚刚读过这个邮件列表

\n
\n

虚拟线程喜欢阻塞 I/O。如果线程需要阻塞(例如 Socket 读取),那么这会释放底层内核线程以执行其他工作

\n
\n

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

\n

Tho*_*ger 20

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

相反,当进行阻塞调用(例如 I/O)时,虚拟线程会自动放弃(或让出)其承载线程。这是由库和运行时处理的[...]

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

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

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

  • 因此,如果我想使用虚拟线程从套接字读取数据并使用InputStream,JVM运行时会注意到它,而不是在Linux中使用阻塞操作系统线程的系统调用read(),而是用epoll()之类的东西替换它,添加一个回调并稍后取消虚拟线程以执行回调。我对吗 ? (2认同)

Alm*_*zak 11

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

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

JVM 运行时会将其转换为以下堆栈跟踪

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)
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

文章作者使用MacOs,Mac使用kqueue非阻塞syscall,如果我在Linux上运行它,我会看到epollsyscall。

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

  • _当我调用 InputStream.read() 时,我确实期望有一个相应的 read() 系统调用_:你为什么这么期望?因为目前的实施呢?最后,对于您的代码来说,执行相应的 read() 系统调用并不重要,重要的是从“InputStream.read()”返回时数据已被读取。其他一切都应该是实现细节。 (9认同)
  • 我个人认为了解执行哪个系统调用很重要,它使我能够了解 IO 包在 java 中的工作原理,并且在性能下降的情况下,我可以从我的生产计算机中看到系统调用,并检查哪个进程表现不佳以及原因。 (4认同)
  • “InputStream”并不总是“FileInputStream”。有“ByteArrayInputStream”,即使实际从文件读取时,“FileInputStream”也可能被包装在“BufferedInputStream”中,但它也可能是包装“FileInputStream”的“ZipInputStream”,但并不是每个“read”调用都会结束在系统调用中。即使它最终出现在系统调用中,也从未有规范表明系统调用必须与此方法具有相同的名称。如果您认为您必须知道发生了什么,那也没关系,但实施没有义务遵循您的期望。 (4认同)