Project loom:是啥让使用虚拟线程时性能更好?
Posted
技术标签:
【中文标题】Project loom:是啥让使用虚拟线程时性能更好?【英文标题】:Project loom: what makes the performance better when using virtual threads?Project loom:是什么让使用虚拟线程时性能更好? 【发布时间】:2020-12-01 20:33:40 【问题描述】:为了在这里提供一些背景信息,我已经关注项目织机一段时间了。我读过the state of loom。我做过异步编程。
异步编程(由 java nio 提供)在任务等待时将线程返回到线程池,并且竭尽全力不阻塞线程。这带来了很大的性能提升,我们现在可以处理更多请求,因为它们不受操作系统线程数量的直接约束。但是我们在这里失去的是上下文。同一个任务现在不只与一个线程相关联。一旦我们将任务与线程分离,所有的上下文都会丢失。异常跟踪没有提供非常有用的信息,调试很困难。
带有virtual threads
的项目织机将成为单一的并发单元。现在您可以在单个 virtual thread
上执行单个任务。
到目前为止一切都很好,但文章继续说明,项目织机:
一个简单的同步网络服务器将能够处理更多的请求,而不需要更多的硬件。
我不明白我们如何通过异步 API 获得项目织机的性能优势? asynchrounous APIs
确保不要让任何线程空闲。那么,project loom 做了哪些工作来使其比asynchronous
API 更高效、更高效?
编辑
让我重新表述这个问题。假设我们有一个 http 服务器,它接收请求并使用支持的持久数据库执行一些 crud 操作。比如说,这个 http 服务器处理了很多请求 - 100K RPM。两种实现方式:
-
HTTP 服务器有一个专用的线程池。当一个请求进来时,一个线程将任务向上传送直到它到达DB,其中任务必须等待来自DB的响应。此时,线程返回线程池并继续执行其他任务。当 DB 响应时,它会再次由线程池中的某个线程处理并返回 HTTP 响应。
HTTP 服务器只是为每个请求生成
virtual threads
。如果有 IO,虚拟线程只是等待任务完成。然后返回 HTTP 响应。基本上,virtual threads
没有进行池业务。
鉴于硬件和吞吐量保持不变,任何一种解决方案在响应时间或处理更多吞吐量方面是否会比另一种解决方案更好?
我的猜测是性能不会有任何差异。
【问题讨论】:
The answer you got,很短,但很到位。除此之外,您已经链接到a document that explains the concept in great detail。我建议阅读它,尤其是它解释虚拟线程如何在另一个执行器的 atop 上运行的部分,例如线程池以及同步调用如何被异步对应物替换。这使得第二种方法转变为第一种方法。那么您希望从赏金中获得哪些额外信息? 仅供参考,Oracle 的 Ron Pressler 于 2020 年底就 Project Loom 技术发表演讲:here 和 here。 【参考方案1】:@talex 的answer 说得很清楚。进一步补充。
Loom 更多的是关于原生并发抽象,它还有助于编写异步代码。鉴于它是一个 VM 级别的抽象,而不仅仅是代码级别(就像我们迄今为止对 CompletableFuture
所做的那样),它可以让人们实现异步行为但减少样板。
使用 Loom,一个更强大的抽象是救星。我们已经反复看到这一点,关于如何使用语法糖进行抽象,使一个人有效地编写程序。无论是JDK8中的FunctionalInterfaces,还是Scala中的理解。
使用 loom,不需要链接多个 CompletableFuture(以节省资源)。但是可以同步编写代码。并且遇到每个阻塞操作(ReentrantLock、i/o、JDBC 调用),虚拟线程都会停止。而且因为这些是轻量级线程,上下文切换更便宜,与内核线程不同。
当被阻塞时,实际的载体线程(正在运行虚拟线程的run
-body)会参与执行其他一些虚拟线程的运行。如此有效地,载体线程并没有闲置,而是执行一些其他工作。并在未停放时返回以继续执行原始虚拟线程。就像线程池的工作方式一样。但是在这里,你有一个单一的载体线程,以一种方式执行多个虚拟线程的主体,在阻塞时从一个切换到另一个。
我们获得与手动编写的异步代码相同的行为(以及性能),但避免了样板做同样的事情。
考虑 web 框架的情况,其中有一个单独的线程池来处理 i/o,另一个用于执行 http 请求。对于简单的 HTTP 请求,可能会为来自 http-pool 线程本身的请求提供服务。但是如果有任何阻塞(或)高 CPU 操作,我们让这个活动异步发生在一个单独的线程上。
此线程将从传入请求中收集信息,生成CompletableFuture
,并将其与管道链接(从数据库作为一个阶段读取,然后从它进行计算,然后是另一个阶段以写回数据库案例,网络服务调用等)。每一个都是一个阶段,结果CompletablFuture
被返回给web-framework。
当生成的未来完成时,网络框架使用结果中继回客户端。这就是Play-Framework
和其他人一直在处理它的方式。在 http 线程处理池和每个请求的执行之间提供隔离。但如果我们深入研究,我们为什么要这样做?
一个核心原因是有效利用资源。特别是阻塞呼叫。因此,我们使用thenApply
等进行链接,这样任何活动都不会阻塞任何线程,我们可以用更少的线程做更多的事情。
这很好用,但非常冗长。而且调试确实很痛苦,如果其中一个中间阶段出现异常,控制流就会变得混乱,导致进一步的代码来处理它。
使用 Loom,我们编写同步代码,并让其他人决定在阻塞时做什么。与其睡觉什么都不做。
【讨论】:
This works great, but quite verbose. And debugging is indeed painful, and if one of the intermediary stages results with an exception, the control-flow goes hay-wire, resulting in further code to handle it.
为什么说它很冗长?而这里的哪一部分是冗长的?未来的部分?
是的。话虽如此,我认为未来在其他几种情况下是不可避免的......尤其是当一个人想要进行并行活动时(比如进行多个 Web 服务调用并合并结果)。但是这些活动中的每一个都可能是同步的..【参考方案2】:
http 服务器有一个专用的线程池...... 有多大的游泳池? (CPU 数量)*N + C? N>1 可以回退到抗缩放,因为锁争用会延长延迟;其中 N=1 可能无法充分利用可用带宽。有很好的分析here。
http 服务器刚刚生成... 那将是这个概念的一个非常幼稚的实现。一个更现实的方法是努力从动态池中收集,该池为每个阻塞的系统调用保留一个真正的线程,为每个真正的 CPU 保留一个线程。至少 Go 背后的人是这么想的。
关键是要让handlers, callbacks, completions, virtual threads, goroutines : all PEAs in a pod不争夺内部资源;因此,除非绝对必要,否则它们不会依赖基于系统的阻塞机制。这属于锁避免的旗帜,并且可以通过各种排队策略(参见 libdispatch)等来完成。请注意,这会使PEA 与底层系统线程分离,因为它们在内部是多路复用的。这是您对分离概念的担忧。在实践中,您可以传递您最喜欢的语言抽象上下文指针。
正如1 所指出的,有一些切实的结果可以直接与这种方法相关联;和一些无形资产。锁定很容易——你只需在你的交易周围做一个大锁,你就可以开始了。那不成比例;但是细粒度的锁定很难。上工难,粮食细度难选。什么时候使用锁、CVs、信号量、障碍、...在教科书的例子中很明显;在深度嵌套的逻辑中少一些。在大多数情况下,锁避免使这种情况消失,并仅限于诸如 malloc() 之类的竞争叶组件。
我对此持怀疑态度,因为研究通常表明系统扩展性较差,该系统被转换为锁避免模型,然后被证明更好。我还没有看到让一些经验丰富的开发人员 来分析系统的同步行为,将其转换为可扩展性,然后测量结果的方法。但是,即使那是一场胜利,经验丰富的开发人员也是一种稀有(ish)且昂贵的商品;可扩展性的核心实际上是财务。
【讨论】:
【参考方案3】:我们没有从异步 API 中获益。我们可能会获得类似于异步的性能,但使用的是同步代码。
【讨论】:
没错。编写同步代码通常更容易,因为您不必在每次无法取得进展时都继续编写代码来放下并重新拾起它们。直截了当的“这样做,然后那样做,如果发生这种情况,做另一件事”代码比更新显式状态的状态机更容易编写(并且更容易维护)。虚拟线程可以为您提供异步代码的大部分好处,同时您的编码体验更接近于编写同步代码。 @DavidSchwartz 在分析异常或调试代码时更是如此。此外,使用虚拟线程取消正在进行的操作要容易得多。并且线程本地变量将按预期工作。但这一切都已经在链接问题的文档中进行了解释……以上是关于Project loom:是啥让使用虚拟线程时性能更好?的主要内容,如果未能解决你的问题,请参考以下文章