如何在不支持 JVM 的 JVM lang 中实现协程?

2022-08-31 19:36:00

这个问题是在阅读了Loom提案之后出现的,该提案描述了一种在Java编程语言中实现协程的方法。

特别是这个提案说,要在语言中实现这个特性,将需要额外的JVM支持。

据我所知,JVM上已经有几种语言将协程作为其功能集的一部分,例如Kotlin和Scala。

那么,如何在没有额外支持的情况下实现此功能,如果没有它,是否可以有效地实现它?


答案 1

tl;博士总结:

特别是这个建议说,要用语言实现这个特性,将需要额外的JVM支持。

当他们说“必需”时,他们的意思是“必需的,以便以一种既高性能又可以在语言之间互操作的方式实现”。

因此,如何在没有额外支持的情况下实现此功能

有很多方法,最容易理解的是如何工作(但不一定是最容易实现的)是在JVM之上实现你自己的VM和你自己的语义。(请注意,这不是实际完成的方式,这只是为什么可以完成的直觉。

没有它,它能有效地实施吗?

没有。

稍长的解释

请注意,Project Loom 的一个目标是纯粹作为库引入此抽象。这有三个优点:

  • 引入新库比更改 Java 编程语言要容易得多。
  • 库可以立即由用JVM上的每种语言编写的程序使用,而Java语言特性只能由Java程序使用。
  • 可以实现具有不使用新 JVM 功能的相同 API 的库,这将允许您编写在较旧的 JVM 上运行的代码,只需进行简单的重新编译(尽管性能较低)。

但是,将其实现为库会阻止聪明的编译器技巧将协同例程转换为其他内容,因为不涉及编译器。如果没有聪明的编译器技巧,获得良好的性能就要困难得多,因此,这是JVM支持的“要求”。

更长的解释

通常,所有通常的“强大”控制结构在计算意义上都是等效的,并且可以相互实现。

那些“强大的”通用控制流结构中最有名的是古老的,另一个是延续。然后,有线程和协程,人们不经常想到的,但这也等同于:异常。GOTOGOTO

另一种可能性是重新定义的调用堆栈,以便程序员可以将调用堆栈作为对象进行访问,并且可以对其进行修改和重写。(例如,许多Smalltalk方言都这样做,这也有点像在C和汇编中完成此操作的方式。

只要您拥有其中之,您就可以通过在另一个之上实现一个来拥有所有这些

JVM有两个:异常和,但JVM中的不是通用的,它非常有限:它只在单个方法工作。(它本质上仅用于循环。所以,这给我们留下了例外。GOTOGOTO

因此,这是您问题的一个可能答案:您可以在异常之上实现协同例程。

另一种可能性是根本不使用 JVM 的控制流,而是实现自己的堆栈。

但是,这通常不是在 JVM 上实现协同例程时实际采用的路径。最有可能的是,实现协同例程的人会选择使用蹦床,并将部分执行上下文重新定位为对象。例如,生成器是如何在CLI上用C实现♯的(不是JVM,但挑战是相似的)。C♯ 中的生成器(基本上是受限制的半协同例程)是通过将方法的局部变量提升到上下文对象的字段中并在每个语句中将方法拆分为该对象上的多个方法,将它们转换为状态机,并通过上下文对象上的字段仔细地线程化所有状态更改来实现的。在/作为语言功能出现之前,聪明的程序员也使用相同的机器实现了异步编程。yieldasyncawait

然而,这就是你指出的文章最有可能提到的:所有这些机器都是昂贵的。如果您实现自己的堆栈或将执行上下文提升到单独的对象中,或者将所有方法编译为一个巨大的方法并在任何地方使用(由于方法的大小限制,这甚至是不可能的),或者使用 Exceptions 作为控制流,那么这两件事中至少有一件是正确的:GOTO

  • 您的调用约定变得与其他语言期望的JVM堆栈布局不兼容,即您失去了互操作性
  • JIT编译器不知道你的代码到底在做什么,并且呈现出字节代码模式,执行流模式和使用模式(例如,抛出和捕获大量的异常),它不期望也不知道如何优化,即你失去了性能

Rich Hickey(Clojure的设计师)曾经在一次演讲中说过:“Tail Calls,Performance,Interop。选择两个。我将其推广到我称之为Hickey的Maxim:“高级控制流,性能,互操作。选择两个。

事实上,通常很难实现一互操作或性能。

此外,您的编译器将变得更加复杂。

当构造在 JVM 中本机可用时,所有这些都会消失。例如,想象一下,如果JVM没有线程。然后,每个语言实现都将创建自己的线程库,该库是困难的,复杂的,缓慢的,并且不与任何其他语言实现的线程库进行互操作。

最近一个现实世界的例子是lambdas:JVM上的许多语言实现都有lambda,例如Scala。然后Java也添加了lambda,但是由于JVM不支持lambda,因此必须以某种方式对它们进行编码,并且Oracle选择的编码与Scala之前选择的编码不同,这意味着您无法将Java lambda传递给期望Scala的Scala方法。在这种情况下,解决方案是Scala开发人员完全重写了他们的lambda编码,以便与Oracle选择的编码兼容。这实际上在某些地方破坏了向后兼容性。Function


答案 2

来自 Kotlin Documentation on Coroutines(强调我的):

协程通过将复杂性放入库中来简化异步编程。程序的逻辑可以在协程中按顺序表示,底层库将为我们找出异步性。该库可以将用户代码的相关部分包装到回调中,订阅相关事件,在不同的线程(甚至不同的机器)上安排执行,并且代码保持简单,就好像它是按顺序执行的一样。

长话短说,它们被编译成使用回调和状态机来处理挂起和恢复的代码。

项目负责人罗曼·伊丽莎罗夫(Roman Elizarov)在KotlinConf 2017上就这一主题进行了两次精彩的演讲。一个是协程简介,第二个是协程的深入探讨


推荐