Project loom:使用虚拟线程时是什么让性能更好?

Project loom: what makes the performance better when using virtual threads?

为了在这里提供一些背景信息,我关注 Loom 项目已有一段时间了。我已阅读 the state of loom。我做过异步编程

异步编程(由javanio提供)returns任务等待时线程到线程池,竭尽全力不阻塞线程。这带来了很大的性能提升,我们现在可以处理更多的请求,因为它们不受 OS 线程数量的直接限制。但是我们在这里失去的是上下文。同一任务现在不再只与一个线程相关联。一旦我们将任务与线程分离,所有上下文都会丢失。异常跟踪没有提供非常有用的信息,调试很困难。

带有 virtual threads 的项目织机成为单个并发单元。现在您可以在单个 virtual thread.

上执行单个任务

到现在为止一切都很好,但是文章接着说,用 project loom:

A simple, synchronous web server will be able to handle many more requests without requiring more hardware.

我不明白我们如何通过异步 API 的项目织机获得性能优势? asynchrounous APIs 确保不要让任何线程空闲。那么,loom 项目做了什么来使其比 asynchronous API 更高效、更高效?

编辑

让我重新表述一下这个问题。假设我们有一个 http 服务器,它接收请求并使用支持的持久数据库执行一些 crud 操作。比方说,这个 http 服务器处理很多请求——100K RPM。两种实现方式:

  1. HTTP 服务器有一个专用的线程池。当请求进来时,一个线程携带任务向上,直到它到达DB,其中任务必须等待来自DB的响应。此时,线程返回到线程池,继续执行其他任务。当 DB 响应时,它再次由线程池中的某个线程处理,并且它 returns 一个 HTTP 响应。
  2. HTTP 服务器为每个请求生成 virtual threads。如果有IO,虚拟线程只是等待任务完成。然后 returns HTTP 响应。基本上,virtual threads.
  3. 没有合并业务。

假设硬件和吞吐量保持不变,在响应时间或处理更多吞吐量方面,任何一种解决方案是否比另一种更好?

我的猜测是 w.r.t 性能不会有任何差异。

我们没有从异步 API 中获益。我们可能会获得类似于异步的性能,但使用同步代码。

  1. http 服务器有一个专用的线程池.... 游泳池有多大? (CPU 的数量)*N + C? N>1 可以回退到 anti-scaling,因为锁争用会延长延迟;其中 N=1 可以 under-utilize 可用带宽。有一个很好的分析here.

  2. http 服务器刚刚生成... 这将是这个概念的一个非常幼稚的实现。更现实的做法是努力从动态池中进行收集,动态池为每个阻塞的系统调用保留一个真实线程 + 每个真实 CPU 一个线程。至少这是 Go 背后的人们想出的。

关键是要防止{处理程序、回调、完成、虚拟线程、goroutines:一个 pod 中的所有 PEAs} 不争夺内部资源;因此,除非绝对必要,否则它们不会依赖基于系统的阻塞机制。这属于 锁避免 的旗帜,并且可以通过各种排队策略(参见 libdispatch)等来实现。请注意这使得 PEA 与底层系统线程分离,因为它们在内部是多路复用的。这是您对分离概念的关注。实际上,您传递上下文指针的您最喜欢的语言抽象。

1 所示,有一些切实的结果可以直接与这种方法联系起来;和一些无形资产。锁定很容易——您只需在交易周围锁定一个大锁就可以了。那没有规模;但是 fine-grained 锁定很难。劳作难,谷粒细度难选。何时使用 { locks, CVs, semaphores, barriers, ... } 在教科书的例子中很明显;在深层嵌套的逻辑中稍微少一点。在大多数情况下,锁避免使得它消失,并且仅限于竞争的叶组件,如 malloc()。

我持怀疑态度,因为研究通常显示一个扩展性差的系统,将其转换为锁避免模型,然后显示更好。我还没有看到可以释放一些 有经验的开发人员 来分析系统的同步行为,将其转换为可伸缩性,然后测量结果的方法。但是,即使那是一场胜利 有经验的开发人员 也是一种稀有且昂贵的商品;可扩展性的核心是财务。

@talex 的 说得很清楚。进一步添加。

Loom 更多的是一种本机并发抽象,它还有助于编写异步代码。考虑到它是 VM 级别的抽象,而不仅仅是代码级​​别(就像我们到目前为止一直在做的 CompletableFuture 等),它可以实现异步行为但减少样板。

有了 Loom,一个更强大的抽象就是救世主。我们已经反复看到这一点,关于语法糖的抽象如何使一个人有效地编写程序。无论是JDK8中的FunctionalInterfaces,还是Scala中的for-comprehensions

使用 loom,无需链接多个 CompletableFuture(以节省资源)。但是可以同步编写代码。在遇到每个阻塞操作(ReentrantLock、i/o、JDBC 调用)时,虚拟线程都会停止。而且因为这些是轻量级线程,上下文切换更便宜,与内核线程区分开来。

当阻塞时,实际的载体线程(即 运行 虚拟线程的 run 主体)开始执行其他虚拟线程的 运行 .如此有效,载体线程并没有闲置,而是在执行其他一些工作。并在未停放时返回继续执行原始虚拟线程。就像线程池的工作方式一样。但是在这里,您有一个单一的载体线程以一种方式执行多个虚拟线程的主体,当阻塞时从一个切换到另一个。

我们得到了与手动编写的异步代码相同的行为(以及性能),但是避免了样板代码做同样的事情。


考虑一个 web 框架的情况,其中有一个单独的线程池来处理 i/o,另一个用于执行 http 请求。对于简单的 HTTP 请求,可以从 http-pool 线程本身处理请求。但是如果有任何阻塞(或)高 CPU 操作,我们让这个 activity 在一个单独的线程上异步发生。

该线程将从传入请求中收集信息,生成一个 CompletableFuture,并将其与管道链接(作为一个阶段从数据库中读取,然后从中计算,然后在另一个阶段写入回到数据库案例、Web 服务调用等)。每个都是一个阶段,结果 CompletablFuture 返回到网络框架。

当结果未来完成时,网络框架使用结果转发回客户端。 Play-Framework 和其他人就是这样处理的。在 http 线程处理池和每个请求的执行之间提供隔离。但如果我们深入研究,我们为什么要这样做?

一个核心原因是有效利用资源。特别是阻塞调用。因此,我们使用 thenApply 等进行链接,这样就不会在任何 activity 上阻塞任何线程,并且我们可以用更少的线程做更多的事情。

这很好用,但是冗长。调试确实很痛苦,如果其中一个中间阶段出现异常,控制流就会变得混乱,导致需要进一步的代码来处理它。

使用 Loom,我们编写同步代码,让其他人决定在阻塞时做什么。而不是睡觉什么都不做。