在没有JVM支持的情况下,如何在JVM语言中实现协程?

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

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

据我所知,JVM上已经有几种语言,它们的协同程序是Kotlin和Scala等function集的一部分。

那么如何在没有额外的支持的情况下实现这个function呢?

tl; dr总结:

特别是这个建议表示,要在语言中实现这个function,将需要额外的JVM支持。

当他们说“必需”时,他们的意思是“为了在语言之间高效并且可以互操作的方式来实施”。

所以如何在没有额外支持的情况下实现这个功

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

没有它可以有效地实施吗?

不是真的。

稍微长一点的解释

请注意,Project Loom的一个目标就是将这个抽象纯粹作为一个库来引入。 这有三个好处:

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

然而,将它作为一个库来实现会阻碍聪明的编译器技巧,从而将协程变成别的东西,因为不涉及编译器 。 如果没有聪明的编译器技巧,获得好的性能是非常困难的,对于JVM支持来说是“要求”。

更长的解释

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

那些“强大的”通用控制流结构中最为人熟知的是GOTO ,另外一个是Continuation。 然后,有线程和协程,人们经常不会想到,但这也相当于GOTO :exception。

不同的可能性是重新调用堆栈,因此调用堆栈可作为程序员的对象访问,并且可以被修改和重写。 (例如,许多Smalltalk方言都是这样做的,而且它也是如此 – 就像在C和程序集中做的那样)。

只要你有其中之一,你就可以拥有所有这些,只要一个在另一个之上。

JVM有两个:Exceptions和GOTO ,但是JVM中的GOTO 不是通用的,它是非常有限的:它只能一个方法中工作。 (它本质上只适用于循环。)所以,这给我们留下了例外。

所以,这是对您的问题的一个可能的答案:您可以在Exceptions之上实现协同例程。

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

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

但是 ,这就是你指出的文章中最有可能提到的:所有这些机器都是昂贵的。 如果你实现自己的栈或者将执行上下文放到一个单独的对象中,或者把所有的方法编译成一个巨大的方法并且在任何地方使用GOTO (这是因为方法的大小限制而不可能的),或者使用exception作为控制流,这两件事至少有一个是真的:

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

Rich Hickey(Clojure的设计者)在一次演讲中曾经说过:“尾巴呼叫,表演,Interop。Pick Two”。 我把它推广到我称之为Hickey的Maxim :“先进的控制 – 流程,性能,互操作性。

事实上,即使互操作或性能之一也很难实现。

而且,你的编译器会变得更复杂。

所有这些都会消失,当构造在JVM中本地可用时。 想象一下,例如,如果JVM没有线程。 然后,每个语言实现都会创建自己的Threading库,这个库很难,复杂,速度慢,并且不能与任何其他语言实现的线程库交互操作。

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

从Kotlin Coroutines文档 (重点介绍):

协程通过将并发程序放入库来简化异步编程。 程序的逻辑可以在协同程序中顺序expression,底层的库会为我们找出异步。 库可以将用户代码的相关部分包装到回调函数中,订阅相关的事件,在不同的线程 (甚至不同的机器!) 上调度执行 ,代码就像顺序执行一样简单。

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

项目主管Roman Elizarov在2017年KotlinConf上就这个问题进行了两场精彩的演讲。 一个是协程的简介 ,第二个是协程的深入分析 。

协程 并不依赖操作系统或JVM的function 。 相反,协程和suspendfunction由编译器转换,生成一个状态机,能够处理总体上的暂停,并在挂起的协程中保持状态。 这是由Continuations启用的,它们作为参数被编译器添加到每个挂起函数中 ; 这种技术被称为“ 延续传球风格 ”(CPS)。

suspendfunction的转换中可以看到一个例子:

 suspend fun  CompletableFuture.await(): T 

以下显示了CPS转换后的签名:

 fun  CompletableFuture.await(continuation: Continuation): Any? 

如果你想知道更多的细节,你需要阅读这个解释 。