tl;dr 摘要:
该提案特别指出,要在该语言中实现此功能,需要额外的 JVM 支持。
当他们说“必需”时,他们的意思是“需要以这样一种方式实现,即它在语言之间具有高性能和互操作性”。
那么如何在没有额外支持的情况下实现此功能
有很多方法,最容易理解它的工作原理(但不一定最容易实现)是在 JVM 之上用自己的语义实现自己的 VM。 (请注意,不是它实际上是如何完成的,这只是为什么可以做到这一点的直觉。)
没有它还能有效实现吗?
不是真的。
稍微长一点的解释:
请注意,Project Loom 的一个目标是将这种抽象纯粹作为一个库引入。这具有三个优点:
- 引入新库比更改 Java 编程语言容易得多。
- 在 JVM 上以每种语言编写的程序都可以立即使用库,而 Java 语言功能只能由 Java 程序使用。
- 可以实现具有相同 API 且不使用新 JVM 功能的库,这将允许您编写在旧 JVM 上运行的代码,只需重新编译(尽管性能较低)。
但是,将其作为一个库实现会排除巧妙的编译器技巧,将协同例程转换为其他东西,因为不涉及编译器。如果没有聪明的编译器技巧,要获得良好的性能要困难得多,因此,这是 JVM 支持的“要求”。
更长的解释:
一般来说,所有常见的“强大”控制结构在计算意义上都是等效的,并且可以相互使用来实现。
那些“强大”的通用控制流结构中最著名的是古老的GOTO,另一个是Continuations。然后是线程和协程,这是人们不常想到的,但这也相当于GOTO: Exceptions。
另一种可能性是重新定义的调用堆栈,以便程序员可以将调用堆栈作为对象访问,并且可以对其进行修改和重写。 (例如,许多 Smalltalk 方言都是这样做的,这也有点像在 C 和汇编中这样做的方式。)
只要你有一个,你就可以拥有所有,只需在另一个之上实现一个。
JVM 有两个:Exceptions 和 GOTO,但是 JVM 中的GOTO不是通用的,它非常有限:它只能在内部工作> 单一方法。 (它本质上只用于循环。)所以,这给我们留下了例外。
所以,这是您问题的一个可能答案:您可以在异常之上实现协同例程。
另一种可能性是完全不使用 JVM 的控制流并实现自己的堆栈。
但是,这通常不是在 JVM 上实现协同例程时实际采用的路径。最有可能的是,实现协同程序的人会选择使用 Trampolines 并将执行上下文部分地重新定义为一个对象。也就是说,例如,生成器是如何在 CLI 上的 C♯ 中实现的(不是 JVM,但挑战是相似的)。 C♯中的生成器(基本上是受限制的半协同程序)是通过将方法的局部变量提升到上下文对象的字段中并在每个yield语句中将该方法拆分为该对象上的多个方法来实现的,并将它们转换进入状态机,并通过上下文对象上的字段仔细处理所有状态更改。在 async/await 作为语言特性出现之前,一位聪明的程序员也使用相同的机制实现了异步编程。
但是,这就是您所指的文章最有可能提到的内容:所有这些机器都很昂贵。如果您实现自己的堆栈或将执行上下文提升到单独的对象中,或者将所有方法编译为一个 giant 方法并在任何地方使用GOTO(由于大小限制,这甚至是不可能的在方法上),或者使用异常作为控制流,这两种情况中至少有一种是正确的:
- 您的调用约定与其他语言所期望的 JVM 堆栈布局不兼容,即您失去了互操作性。
- JIT 编译器不知道您的代码到底在做什么,并且会显示字节码模式、执行流模式和使用模式(例如,抛出和捕获 巨大 数量的异常)它不期望也不知道如何优化,即你失去了性能。
Rich Hickey(Clojure 的设计师)曾经在一次演讲中说:“尾调用、性能、互操作。选择两个。”我将其概括为我称之为 Hickey 的格言:“高级控制流、性能、互操作。选择两个。”
事实上,即使是一个互操作或性能通常也很难实现。
另外,你的编译器会变得更复杂。
当构造在 JVM 中本机可用时,所有这些都会消失。例如,想象一下,如果 JVM 没有线程。然后,每种语言实现都将创建自己的线程库,这很难、复杂、缓慢,并且不与任何其他语言实现的线程库互操作。
最近的一个现实世界的例子是 lambdas:JVM 上的许多语言实现都有 lambdas,例如斯卡拉。然后 Java 也添加了 lambda,但由于 JVM 不支持 lambda,它们必须以某种方式进行 编码,并且 Oracle 选择的编码与 Scala 之前选择的编码不同,这意味着你无法将 Java lambda 传递给期望 Scala Function 的 Scala 方法。在这种情况下,解决方案是 Scala 开发人员完全重写了他们的 lambda 编码,以与 Oracle 选择的编码兼容。这实际上在某些地方破坏了向后兼容性。