在没有JVM支持的情况下,如何在JVM语言中实现协程? [英] How are coroutines implemented in JVM langs without JVM support?

查看:112
本文介绍了在没有JVM支持的情况下,如何在JVM语言中实现协程?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在阅读织机提案后,出现了这个问题,描述了一种在Java编程语言中实现协同程序的方法。



特别是这个提议说,要用该语言实现这个功能,需要额外的JVM支持。 / p>

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



那么如果没有额外的支持就可以实现这个功能,没有它可以有效地实现吗?

解决方案

tl; dr 摘要:


特别是此提案表示要使用该语言实现此功能,其他JVM支持将需要。


当他们说必需时,他们的意思是必填命令以这样的方式实现,即它在语言之间具有高效性和可互操作性。


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


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


如果没有它可以有效实施吗?


不是真的。



稍微长一点的解释



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




  • 引入新库要比更改Java编程语言容易得多。

  • JVM上用每种语言编写的程序可以立即使用库,而Java程序只能使用Java语言功能。

  • 带有库的库可以实现不使用新JVM功能的相同API,这将允许您通过简单的重新编译来编写在较旧的JVM上运行的代码(尽管性能较低)。



然而,将它作为库实现排除了巧妙的编译器技巧,将协同例程转换为其他东西,因为不涉及编译器。如果没有聪明的编译技巧,获得良好的性能会更加困难,因为这是JVM支持的要求。



更长的解释



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



最着名的那些强大的通用控制流结构是古老的 GOTO ,另一个是Continuations。然后,有线程和协同程序,以及人们通常不会想到的,但这也相当于 GOTO :异常。



另一种可能性是重新调用的调用堆栈,因此调用堆栈可作为程序员的对象访问,并且可以进行修改和重写。 (例如,许多Smalltalk方言就是这样做的,它也很像C和汇编中的方式。)



只要你有一个,你可以所有,只需在另一个上面实现一个。



JVM有两个:例外和 GOTO ,但JVM中的 GOTO 不是通用,它是非常有限的:它只在里面单一方法。 (它主要用于循环。)因此,这给我们留下了例外。



所以,这是你的问题的一个可能的答案:你可以实现协同例程除了例外。



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



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



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




  • 您的调用约定与其他语言期望的JVM堆栈布局不兼容,即您失去了互操作性

  • JIT编译器不知道您的代码到底在做什么,并提供字节代码模式,执行流模式和使用模式(例如,抛出和捕获 ginormous 数量的异常),它没有预料到并且不知道如何优化,即你失去了性能



Rich Hickey(Clojure的设计师)曾说过一个谈话:尾巴呼唤,表演,互操作。选择两个。我将其概括为我称之为 Hickey的Maxim :高级控制流程,性能,互操作。选择两个。



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



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



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



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


This question came up after reading the Loom proposal, which describes an approach of implementing coroutines in the Java programming language.

Particularly this proposal says that to implement this feature in the language, additional JVM support will be required.

As I understand it there are already several languages on the JVM that have coroutines as part of their feature set such as Kotlin and Scala.

So how is this feature implemented without additional support and can it be implemented efficiently without it?

解决方案

tl;dr Summary:

Particularly this proposal says that to implement this feature in the language the additional JVM support will be required.

When they say "required", they mean "required in order to be implemented in such a way that it is both performant and interoperable between languages".

So how this feature is implemented without additional support

There are many ways, the most easy to understand how it can possibly work (but not necessarily easiest to implement) is to implement your own VM with your own semantics on top of the JVM. (Note that is not how it is actually done, this is only an intuition as to why it can be done.)

and can it be implemented efficiently without it ?

Not really.

Slightly longer explanation:

Note that one goal of Project Loom is to introduce this abstraction purely as a library. This has three advantages:

  • It is much easier to introduce a new library than it is to change the Java programming language.
  • Libraries can immediately be used by programs written in every single language on the JVM, whereas a Java language feature can only be used by Java programs.
  • A library with the same API that does not use the new JVM features can be implemented, which will allow you to write code that runs on older JVMs with a simple re-compile (albeit with less performance).

However, implementing it as a library precludes clever compiler tricks turning co-routines into something else, because there is no compiler involved. Without clever compiler tricks, getting good performance is much harder, ergo, the "requirement" for JVM support.

Longer explanation:

In general, all of the usual "powerful" control structures are equivalent in a computational sense and can be implemented using each other.

The most well-known of those "powerful" universal control-flow structures is the venerable GOTO, another one are Continuations. Then, there are Threads and Coroutines, and one that people don't often think about, but that is also equivalent to GOTO: Exceptions.

A different possibility is a re-ified call stack, so that the call-stack is accessible as an object to the programmer and can be modified and re-written. (Many Smalltalk dialects do this, for example, and it is also kind-of like how this is done in C and assembly.)

As long as you have one of those, you can have all of those, by just implementing one on top of the other.

The JVM has two of those: Exceptions and GOTO, but the GOTO in the JVM is not universal, it is extremely limited: it only works inside a single method. (It is essentially intended only for loops.) So, that leaves us with Exceptions.

So, that is one possible answer to your question: you can implement co-routines on top of Exceptions.

Another possibility is to not use the JVM's control-flow at all and implement your own stack.

However, that is typically not the path that is actually taken when implementing co-routines on the JVM. Most likely, someone who implements co-routines would choose to use Trampolines and partially re-ify the execution context as an object. That is, for example, how Generators are implemented in C♯ on the CLI (not the JVM, but the challenges are similar). Generators (which are basically restricted semi-co-routines) in C♯ are implemented by lifting the local variables of the method into fields of a context object and splitting the method into multiple methods on that object at each yield statement, converting them into a state machine, and carefully threading all state changes through the fields on the context object. And before async/await came along as a language feature, a clever programmer implemented asynchronous programming using the same machinery as well.

HOWEVER, and that is what the article you pointed to most likely referred to: all that machinery is costly. If you implement your own stack or lift the execution context into a separate object, or compile all your methods into one giant method and use GOTO everywhere (which isn't even possible because of the size limit on methods), or use Exceptions as control-flow, at least one of these two things will be true:

  • Your calling conventions become incompatible with the JVM stack layout that other languages expect, i.e. you lose interoperability.
  • The JIT compiler has no idea what the hell your code is doing, and is presented with byte code patterns, execution flow patterns, and usage patterns (e.g. throwing and catching ginormous amounts of exceptions) it doesn't expect and doesn't know how to optimize, i.e. you lose performance.

Rich Hickey (the designer of Clojure) once said in a talk: "Tail Calls, Performance, Interop. Pick Two." I generalized this to what I call Hickey's Maxim: "Advanced Control-Flow, Performance, Interop. Pick Two."

In fact, it is generally hard to achieve even one of interop or performance.

Also, your compiler will become more complex.

All of this goes away, when the construct is available natively in the JVM. Imagine, for example, if the JVM didn't have Threads. Then, every language implementation would create its own Threading library, which is hard, complex, slow, and doesn't interoperate with any other language implementation's Threading library.

A recent, and real-world, example are lambdas: many language implementations on the JVM had lambdas, e.g. Scala. Then Java added lambdas as well, but because the JVM doesn't support lambdas, they must be encoded somehow, and the encoding that Oracle chose was different from the one Scala had chosen before, which meant that you couldn't pass a Java lambda to a Scala method expecting a Scala Function. The solution in this case was that the Scala developers completely re-wrote their encoding of lambdas to be compatible with the encoding Oracle had chosen. This actually broke backwards-compatibility in some places.

这篇关于在没有JVM支持的情况下,如何在JVM语言中实现协程?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆