在阅读描述使用Java编程语言实现协程的方法的Loom提案后,我产生了这个问题。
具体来说,该提案表示为了在语言中实现该功能,需要额外的JVM支持。
据我所知,已经有几种语言在JVM上具有协程作为其特性集,例如Kotlin和Scala。
那么,在没有额外支持的情况下,该功能是如何实现的,且能否有效地实现?
简短总结:
特别地,这个提案表示为了在语言中实现该功能,需要额外的JVM支持。
当他们说“需要”时,他们是指“需要以一种既具有性能又可以在各种语言之间互操作的方式来实现”。
那么如果没有额外的支持,如何实现这个功能?
有很多方法,最容易理解的可能是用自己的语义在JVM上实现自己的虚拟机。(请注意,这不是实际操作的方式,这只是为什么可以这样做的直觉。)
不使用额外支持是否可以高效实现?
不太可能。
稍长解释:
请注意,Project Loom的一个目标是纯粹作为库引入这个抽象。这有三个优点:
但是,将其实现为库会排除巧妙的编译器技巧将协程转换为其他内容的可能性,因为没有涉及编译器。没有巧妙的编译器技巧,要获得良好的性能就变得更加困难,因此“需要”JVM支持。
较长解释:
一般来说,所有通常的“强大”控制结构在计算意义上都是等效的,并且可以使用彼此来实现。
最著名的“强大”通用控制流结构之一是备受推崇的GOTO
,另一个是Continuations。此外还有线程和协程,同样等效于GOTO
但人们不经常考虑的一个是例外。GOTO
,但JVM中的GOTO
不是通用的,它非常有限:它只在单个方法内起作用(基本上只用于循环)。因此,这留下了我们使用Exceptions。yield
语句的多个方法来实现的,将它们转换为状态机,并仔细地通过上下文对象上的字段将所有状态更改线程化。在async
/await
成为一种语言功能之前,一个聪明的程序员也使用了同样的机制来实现异步编程。然而,这正是您提到的文章可能指的:所有这些机制都是昂贵的。如果您实现自己的堆栈或将执行上下文提升为单独的对象,或者将所有方法编译为一个超大方法并在各处使用GOTO
(甚至由于方法大小限制而无法实现),或者将异常用作控制流,那么以下至少有一件事情是真实的:
Rich Hickey(Clojure的设计者)曾在演讲中说过:“尾调用,性能,Interop。选择两个。”我将其概括为我所称的Hickey's Maxim:“高级控制流程,性能,Interop。选择两个。”
实际上,即使实现任何一个互操作性或性能也很困难。
此外,您的编译器将变得更加复杂。
当这个构造在JVM中可以原生使用时,所有这些问题都会消失。例如,想象一下如果JVM没有线程。那么,每种语言实现都会创建自己的线程库,这是困难、复杂、缓慢且与任何其他语言实现的线程库无法互操作。
一个最近的真实例子是lambda:许多JVM上的语言实现都有lambda,如Scala。然后Java也添加了lambda,但是因为JVM不支持lambda,它们必须以某种方式进行编码,而Oracle选择的编码与Scala之前选择的编码不同,这意味着您无法将Java lambda传递给期望Scala Function的Scala方法。在这种情况下的解决方案是,Scala开发人员完全重写了他们的lambda编码以与Oracle选择的编码兼容。这实际上在某些地方破坏了向后兼容性。根据Kotlin关于协程的文档(我强调):
协程通过将复杂性放入库中使异步编程更加简单。程序逻辑可以在协程中顺序表达,底层库会为我们处理好异步问题。库可以将用户代码的相关部分包装成回调函数、订阅相关事件、在不同线程(甚至不同机器)上安排执行,而代码看起来就像按顺序执行一样简单。
简而言之,它们被编译为使用回调和状态机处理挂起和恢复的代码。
该项目的负责人Roman Elizarov在KotlinConf 2017上发表了两次关于此主题的精彩演讲。其中一篇是介绍协程,另一篇则是深入了解协程。
suspend
函数的优秀概述。 - nilTheDevsuspend
函数生成一个状态机,能够处理一般的挂起并传递挂起的协程以保持它们的状态。这是由Continuations实现的,编译器将其作为参数添加到每个挂起函数中;这种技术称为“Continuation-passing style”(CPS)。
一个例子可以看到suspend
函数的变换:
suspend fun <T> CompletableFuture<T>.await(): T
fun <T> CompletableFuture<T>.await(continuation: Continuation<T>): Any?
如果您想了解详细信息,需要阅读解释。
Exception
之上实现它们,那么没有人会使用它们,在这些控制流程之上实现(至少在Java中 - 即使是空的堆栈跟踪)将是昂贵的。其次,您关于lambdas
的说法只有部分正确,它们确实具有字节码指令,让运行时决定这些实现将是什么 - 而不是编译器(invokedynamic
)。 - Eugeneinvokedynamic
和整个LambdametaFactory
机制都是实现细节。Java lambdas早于JSR292,最初是在没有它的情况下实现的。JSR292允许更高效、更紧凑的实现,但并非必需。特别是,Retrolambda项目提供了一个符合标准的Java 8 lambdas和方法引用实现,在Java 7、6或5 JVM上运行,后两者都没有invokedynamic
。invokedynamic
与lambda无关,其目的是加速具有任意语义的虚拟分派,特别是语义... - Jörg W Mittaginvokevirtual
版本,其不匹配invokevirtual
。该版本向程序员公开了 JVM 为invokevirtual
进行的所有巧妙优化技巧,以便于 每个 虚拟派发都能受益于这些优化,而不仅仅是看起来像 Java 的虚拟派发。例如,鸭子类型或多重继承。 - Jörg W Mittag