在阅读Loom proposal之后提出了这个问题,它描述了在Java编程语言中实现协程的方法。
特别是此提案表示要在该语言中实现此功能,将需要额外的JVM支持。
据我所知,JVM上已经有多种语言可以将协同程序作为其功能集的一部分,例如Kotlin和Scala。
那么如果没有额外的支持这个功能是如何实现的呢?没有它可以有效地实现吗?
答案 0 :(得分:37)
tl; dr 摘要:
特别是此提案表示,要使用该语言实现此功能,将需要额外的JVM支持。
当他们说" required"时,他们的意思是要求以这样的方式实施,即它在语言之间具有高效性和可互操作性。
那么如何在没有额外支持的情况下实现此功能
有很多方法,最容易理解它是如何工作的(但不一定最容易实现)是在JVM之上用你自己的语义实现你自己的VM。 (注意不它是如何实际完成的,这只是对为什么可以完成的直觉。)
如果没有它可以有效实施吗?
不是。
稍微长一点的解释:
请注意,Project Loom的一个目标是将此抽象纯粹作为库引入。这有三个好处:
然而,将它作为一个库实现排除了巧妙的编译器技巧,将协同例程转换为其他东西,因为没有涉及编译器。如果没有聪明的编译器技巧,获得良好的性能就会变得更加困难,因为这样做需要"要求"用于JVM支持。
更长的解释:
一般来说,所有常见的"强大的"控制结构在计算意义上是等效的,可以相互实现。
最着名的那些"强大的"通用控制流结构是值得尊敬的GOTO
,另一个是Continuations。然后,有线程和协同程序,以及人们经常不会想到的,但这也等同于GOTO
:异常。
另一种可能性是重新调用堆栈,因此调用堆栈可作为程序员的对象访问,并且可以进行修改和重写。 (例如,许多Smalltalk方言就是这样做的,而且它在C语言和汇编方面也是如此。)
只要您拥有一个,就可以所有,只需在另一个上面实现一个
。 JVM有两个:例外和GOTO
,但JVM中的GOTO
不是通用的,它非常有限:它只能用于在里面一个方法。 (它主要用于循环。)因此,这给我们留下了例外。
因此,这是您的问题的一个可能答案:您可以在例外之上实现协同例程。
另一种可能性是不在所有中使用JVM的控制流并实现自己的堆栈。
但是,这通常不是在JVM上实现协同例程时实际采用的路径。最有可能的是,实现协同例程的人会选择使用Trampolines并将执行上下文部分重新作为对象。也就是说,例如,如何在CLI上的C♯中实现生成器(不是JVM,但挑战类似)。 C♯中的生成器(基本上是受限制的半协同例程)是通过将方法的局部变量提升到上下文对象的字段中并在每个yield
语句处将该方法拆分为该对象上的多个方法来实现的,将它们转换为状态机,并通过上下文对象上的字段仔细线程化所有状态更改。在async
/ await
作为语言特性出现之前,一个聪明的程序员也使用相同的机制实现了异步编程。
HOWEVER ,这就是你所指的文章最有可能提到的:所有这些机器都是昂贵的。如果您实现自己的堆栈或将执行上下文提升到单独的对象中,或者将所有方法编译成一个巨型方法并在任何地方使用GOTO
(这甚至不可能,因为对于方法的大小限制),或使用异常作为控制流,这两件事中至少有一件是真的:
事实上,通常很难实现其中一个互操作或性能。
此外,您的编译器将变得更加复杂。
当构造在JVM中本地可用时,所有这一切都消失了。想象一下,例如,如果JVM没有线程。然后,每个语言实现都会创建自己的Threading库,这个库很难,很复杂,很慢,并且不能与任何其他语言实现的线程库进行互操作。
一个最近的,现实世界的例子是lambdas:JVM上的许多语言实现都有lambdas,例如:斯卡拉。然后Java也添加了lambdas,但由于JVM不支持lambdas,它们必须以某种方式编码,而Oracle选择的编码与Scala之前选择的编码不同,这意味着您无法将Java lambda传递给期望Scala Function
的Scala方法。这种情况下的解决方案是Scala开发人员完全重写了lambda的编码,以便与Oracle选择的编码兼容。这实际上在某些地方打破了向后兼容性。
答案 1 :(得分:20)
来自Kotlin Documentation on Coroutines(强调我的):
协同程序通过将并发症放入库来简化异步编程。程序的逻辑可以在协程中顺序表示,底层库将为我们找出异步。 库可以将用户代码的相关部分包装成回调,订阅相关事件,在不同线程上安排执行(甚至不同的机器!),代码仍然像顺序执行一样简单
简而言之,它们被编译为使用回调和状态机来处理挂起和恢复的代码。
项目负责人Roman Elizarov在2017年KotlinConf上就此主题进行了两次精彩的演讲。一个是Introduction to Coroutines,第二个是Deep Dive on Coroutines。
答案 2 :(得分:3)
协同程序 不依赖于操作系统或JVM 的功能。相反,协同程序和suspend
函数由编译器转换,生成一个状态机,能够处理一般的暂停,并传递挂起的协同程序保持其状态。这是由 Continuations 启用的,编译器将作为参数添加到每个挂起函数;这种技术称为“Continuation-passing style”(CPS)。
在suspend
函数的转换中可以观察到一个例子:
suspend fun <T> CompletableFuture<T>.await(): T
以下显示了CPS转换后的签名:
fun <T> CompletableFuture<T>.await(continuation: Continuation<T>): Any?
如果您想了解详细信息,请阅读此explanation。
答案 3 :(得分:2)
同一作者在Project Loom库之前是Quasar。
这里是docs的引文:
在内部,纤维是连续的,然后在 调度程序。延续捕获了一个瞬时状态 计算,并允许其挂起,然后在以后恢复 从暂停点开始的时间。类星体创造 通过检测(在字节码级别)可继续进行 方法。对于调度,Quasar使用ForkJoinPool,这是一个非常 高效的,窃取工作的多线程调度程序。
每次加载类时,Quasar的检测模块(通常 以Java代理的身份运行)对其进行扫描以查找可挂起的方法。每一个 然后以以下方式检测可挂起方法f: 扫描了对其他可挂起方法的调用。每次致电 可暂停方法g,在该方法之前(和之后)插入一些代码 调用g以将局部变量的状态保存(并恢复)到 光纤的堆栈(光纤管理自己的堆栈),并记录 事实(即对g的调用)是可能的暂停点。在 在“可暂停功能链”的末尾,我们将找到 光纤公园公园通过抛出SuspendExecution挂起光纤 异常(检测阻止您捕获,甚至 如果您的方法包含catch(Throwable t)块。
如果g确实阻塞,则SuspendExecution异常将被捕获 纤维类。唤醒光纤(解开)时,使用方法f 将被调用,然后执行记录将显示我们 封锁了g的通话,因此我们将立即跳至f中的那一行 在其中调用g并调用它。最后,我们将达到实际 暂停点(停车请求),我们将在此处继续执行 通话后立即。 g返回时,插入f中的代码 将从光纤堆栈中恢复f的局部变量。
这个过程听起来很复杂,但是会产生性能开销 不超过3%-5%。
似乎几乎所有纯Java continuation libraries都使用类似的字节码检测方法来捕获和恢复堆栈帧上的局部变量。
只有Kotlin和Scala编译器足够勇敢地实现more detached,并且可能使用CPS transformations来实现更高性能,以声明此处其他一些答案中提到的机器。