Project Loom:Reactive模型和协程进行时(翻译)
Java 15将发布Project Loom的第一个版本。我相信这将改变JVM。在这篇文章中,我想深入探讨一下导致我相信这一点的原因。
首先,我们需要了解核心问题。然后,我将尝试描述以前的技术如何解决它。之后,我们将看到Project Loom采取的方法。最后,我将推断后者可能对生态系统产生什么影响。
Project Loom
我们首先必须记住,很长一段时间以来,计算机只有一个内核。即使这样,还是需要同时运行多个程序:这至少要运行两个操作系统和适当的程序。
为了实现并行性的幻觉,它依赖于一个技巧。它运行一个程序,如果该程序没有在特定的时间范围内完成,它将存储其状态以供以后使用。然后,它运行下一个要运行的程序。有几种算法可用来调度下一个程序:循环调度,加权循环调度等。好处是,所有这些都可以通过操作系统线程的概念很好地从开发人员中抽象出来。该操作系统通吃繁重的护理,包括存储执行的状态。但是,线程有两个缺点:
- 线程很重,因为它带有很多状态
- 线程需要大量的机器资源来创建
在所有情况下,现代OS都允许M个线程,其中M是数千个线程。现代机器是多核的,提供N个核,比M少两个数量级。拥有多个内核(无论是物理内核还是虚拟内核)并不会从根本上改变底层机制:M远高于N,并且OS负责从线程到内核的映射。
阻塞线程
面的模型在传统方案中效果很好,但在网络方案中效果不佳。假设有一个Web服务器需要响应HTTP请求。在过去不太好的时候,CGI是处理请求的一种方法。它将每个请求映射到一个流程,以便处理创建整个新流程所需的请求,并在发送响应后将其清除。
Java EE应用程序服务器极大地改善了这种情况,因为实现将线程保留在池中以供以后重用。但是,想象一下响应的生成需要时间,例如因为它需要访问数据库以读取数据。在数据库返回数据之前,线程需要等待。
这意味着线程实际上正在等待其生命周期的大部分时间。一方面,此类线程自己不使用任何CPU。另一方面,它使用其他种类的资源,尤其是内存。
同样,太多线程是操作系统的负担:操作系统必须在数量有限的CPU内核上平衡大量线程。这花费了宝贵的CPU周期,因此,操作系统正在与应用程序竞争CPU。
现在,并发请求的数量可以远远超过服务器可用的线程数量。因此,阻塞的线程浪费资源,并使服务器无响应。为了解决这个问题,可以添加更多的Web服务器来处理负载:这是水平缩放。
在大多数情况下,水平缩放就足够了。等待阻塞线程所花费的时间是浪费的,但是没有任何相关的费用...除非一个人的基础架构位于'云'中。在那种情况下,人们要为未使用的资源付费:这绝不是一个明智的主意。
单线程,反应式和Kotlin协程模型
对于开发人员来说,管理多个线程很复杂。例如,如果您一直在使用(或开发)Swing应用程序,则可能知道“灰色矩形效果”。当用户与窗口的交互(例如单击)启动长时间运行的任务时,就会发生这种情况。如果将另一个窗口移到Swing窗口上,然后再移出,Swing不会重画另一个窗口与Swing窗口相交的区域,而不会留下难看的灰色矩形。原因是长时间运行的任务是在“ 事件调度线程”上启动的,而不是在专用线程上启动的。而且这很容易避免,甚至不涉及在共享的可变状态上进行同步!
为了避免这种情况,某些堆栈完全禁止开发人员使用多个线程。例如,Node.js的API仅提供一个非阻塞事件循环线程:提交的函数采用回调的形式。请注意,这不会阻止实现使用多个线程。
该反应的方法是另一种选择,其实颇为相似。尽管它摆脱了单线程API的限制,并提供了反压力机制,但它仍然需要非阻塞代码。由于OS线程很昂贵,因此Reactive将它们池化,并在整个应用程序生命周期中重复使用它们。核心过程是从池中获取空闲线程,让其执行代码,然后将线程释放回池中。这就是为什么它需要非阻塞代码的原因:如果代码阻塞了,那么执行线程将不会被释放,并且池将在某一点或另一点耗尽。
我感兴趣地观察了Reactive模型如何像篝火一样在Spring生态系统中传播,尽管我选择站在一边。恕我直言,反应式有几个缺点:
- 编写(和阅读!)反应式代码的思维方式与编写传统代码的思维方式非常不同。我愿意承认改变心态只需要时间,持续时间取决于每个开发人员。
- 尽管真正的开发人员不会调试,但我知道很多人会调试-包括我自己。由于上述线程切换的魔力,要跟踪一段代码及其相关的状态并不容易。这需要足够的工具,例如带有相关插件的 IntelliJ IDEA 。
- 最后,出于相同的原因,传统的堆栈跟踪也无济于事。一些黑魔法可以绕开它。但是,这不是为了胆小者。有关选项的完整列表,请查看此文档。
Kotlin语言提供了Reactive方法的替代方法:协程。简而言之,当使用suspend关键字时,Kotlin编译器会在字节码中生成一个有限状态机。好处是在协程块中调用的函数看起来像是顺序执行的,尽管它们是并行执行的-更确切地说,取决于确切的范围,有可能会执行。
Project Loom和虚拟线程
Reactive模型和Kotlin协程都在客户端代码和JVM线程之间添加了一个额外的抽象层。框架/库的职责是动态地将一个映射到另一个。问题的症结在于JVM线程是OS线程的薄包装:请记住,OS线程创建起来很昂贵,并且数量限制为数千个。
Project Loom的目标是实际上将JVM线程与OS线程解耦。
当我第一次意识到该倡议时,其想法是创建一个称为Fiber(线程,Project Loom,您能抓住麻烦吗?)的抽象。一个Fiber责任是让一个操作系统线程,使其运行代码,释放回池,就像无栈一样。
当前的建议有很大的不同:Fiber它没有使用新的类,而是重新使用了Java开发人员非常熟悉的一个类- java.lang.Thread!
因此,在新的JVM版本中,某些Thread对象可能是重量级的并映射到OS线程,而另一些对象可能是虚拟线程。
Project Loom发布的后续影响
主要问题是,既然JVM API提供了对OS线程的抽象,那么其他抽象(例如响应式和协程)又会变成什么样呢?我对预测不满意,但以下是Reactive /协程背后的公司可能采取的一些态度:
- 正面态度,他们意识到自己的框架不再带来任何附加值,而只是重复。他们停止了开发工作,仅向现有客户提供维护版本。他们帮助说客户迁移到新的ThreadAPI,一些帮助可能是以付费咨询的形式。
- 反面态度,他们在各自的框架中投入了大量的精力之后,他们决定继续进行,好像什么也没有发生。例如,Spring框架负责实际设计一个共享的Reactive API,称为Reactive Streams,没有Spring依赖项。当前有两种实现,RxJava v2和Pivotal的Project Reactor。另一方面,JetBrains宣传Kotlin的协程是并行运行代码的最简单方法。
- 中间态度。这两个框架都将继续其生命,但是会将它们各自的基础实现更改为使用虚拟线程。
由于沉没成本的谬误,排在第一位的可能性极小:销售和市场营销将努力保持其“竞争优势”-无论在他们眼中意味着什么。尽管有些工程师出于相同的原因希望保留现有代码,但其他一些工程师则将努力使用新的API。因此,我也不相信第二名也会发生。但是,我认为这两个工程派之间都发挥着力量,然后它们与市场营销/销售之间将在#3之间找到平衡。
结论
Project Looms将现有的Thread实现方式从OS线程的映射更改为可以表示此类线程或虚拟线程的抽象。就其本身而言,这是一个有趣的举动,它在一个平台上历来比创新更重视向后兼容性。与其他最新的Java版本相比,此功能是真正的游戏规则改变者。一般而言,开发人员应尽快开始熟悉它。打算学习Reactive和协程的开发人员可能应该退后一步,并评估他们是否应该学习新的ThreadAPI- 是否需要。
翻译原文
https://blog.frankel.ch/project-loom-reactive-coroutines/
扩展阅读
Project Loom地址: https://github.com/openjdk/loom
Project Loom现有状态: http://cr.openjdk.java.net/~rpressler/loom/loom/sol1_part1.html