usar reemplaza que para mejor diferencias cual and java scala jvm kotlin jvm-languages

reemplaza - kotlin vs java cual es mejor



¿Cómo se implementan las corrutinas en las laps de JVM sin soporte de JVM? (4)

Esta pregunta surgió después de leer la propuesta de Loom , que describe un enfoque de implementación de corutinas en el lenguaje de programación Java.

Particularmente, esta propuesta dice que para implementar esta característica en el idioma, se requerirá soporte adicional de JVM.

Según tengo entendido, ya hay varios idiomas en la JVM que tienen corotines como parte de su conjunto de características, como Kotlin y Scala.

Entonces, ¿cómo se implementa esta característica sin soporte adicional y puede implementarse eficientemente sin ella?


De la Documentación de Kotlin sobre Corutinas (énfasis mío):

Las rutinas simplifican la programación asincrónica al poner las complicaciones en las bibliotecas. La lógica del programa se puede expresar secuencialmente en una corutina, y la biblioteca subyacente descubrirá la asincronía para nosotros. La biblioteca puede incluir partes relevantes del código de usuario en devoluciones de llamada, suscribirse a eventos relevantes, programar la ejecución en diferentes hilos (¡o incluso en diferentes máquinas!), Y el código sigue siendo tan simple como si se hubiera ejecutado secuencialmente.

Para resumir, se compilan en un código que utiliza devoluciones de llamada y una máquina de estado para manejar la suspensión y la reanudación.

Roman Elizarov, el líder del proyecto, dio dos charlas fantásticas en KotlinConf 2017 sobre este tema. Una es una Introducción a Corutinas , la segunda es una inmersión profunda en Corutinas .


El Proyecto Loom fue precedido por la biblioteca Quasar por el mismo autor.

Aquí hay una cita de sus docs :

Internamente, una fibra es una continuación que luego se programa en un programador. Una continuación captura el estado instantáneo de un cálculo, y permite que se suspenda y luego se reanude en un momento posterior desde el punto donde se suspendió. Quasar crea continuaciones instrumentando (en el nivel de bytecode) métodos suspendibles. Para programar, Quasar usa ForkJoinPool, que es un programador de subprocesos múltiples muy eficaz, que roba el trabajo.

Cada vez que se carga una clase, el módulo de instrumentación de Quasar (usualmente se ejecuta como un agente de Java) lo escanea en busca de métodos suspendibles. Cada método suspendible f se instrumenta de la siguiente manera: se escanea para llamadas a otros métodos suspendibles. Para cada llamada a un método suspendible g, se inserta algún código antes (y después) de la llamada ag que guarda (y restaura) el estado de las variables locales en la pila de la fibra (una fibra maneja su propia pila) y registra el hecho de que esto (es decir, la llamada a g) es un posible punto de suspensión. Al final de esta "cadena funcional suspendible", encontraremos una llamada a Fiber.park. park suspende la fibra emitiendo una excepción SuspendExecution (que la instrumentación impide que capture, incluso si su método contiene un bloque catch (Throwable t)).

Si g de hecho bloquea, la clase Fiber atrapará la excepción SuspendExecution. Cuando la fibra se despierta (con unpark), se llamará al método f, y luego el registro de ejecución mostrará que estamos bloqueados en la llamada a g, por lo que inmediatamente saltaremos a la línea en f donde se llama g, y llámalo. Finalmente, alcanzaremos el punto de suspensión real (la llamada para estacionar), donde reanudaremos la ejecución inmediatamente después de la llamada. Cuando g vuelve, el código insertado en f restaurará las variables locales de f de la pila de fibra.

Este proceso parece complicado, pero incurre en una sobrecarga de rendimiento de no más del 3% -5%.

Parece que casi todas las libraries continuation java puras utilizaron un enfoque de instrumentación de código de bytes similar para capturar y restaurar variables locales en los marcos de pila.

Solo los compiladores de Kotlin y Scala fueron lo suficientemente valientes como para implementar un enfoque más desacoplado y potencialmente más eficiente con las transformaciones de CPS a las máquinas de estado mencionadas en algunas otras respuestas aquí.


Las corutinas no dependen de las características del sistema operativo o la JVM . En su lugar, las corutinas y las funciones de suspend son transformadas por el compilador que produce una máquina de estado capaz de manejar suspensiones en general y de suspender corutinas que mantienen su estado. Esto está habilitado por Continuaciones , que se agregan como un parámetro para cada función suspendida por el compilador; esta técnica se llama " estilo de continuación de la continuidad " (CPS).

Un ejemplo se puede observar en la transformación de las funciones de suspend :

suspend fun <T> CompletableFuture<T>.await(): T

A continuación, se muestra su firma después de la transformación de CPS:

fun <T> CompletableFuture<T>.await(continuation: Continuation<T>): Any?

Si desea conocer los detalles difíciles, debe leer esta explanation .


tl; dr Resumen:

En particular, esta propuesta dice que para implementar esta característica en el idioma se requerirá soporte adicional de JVM.

Cuando dicen "requerido", quieren decir "requerido para ser implementado de tal manera que sea tanto eficaz como interoperable entre idiomas".

Entonces, ¿cómo se implementa esta característica sin soporte adicional?

Hay muchas formas, la forma más fácil de entender cómo puede funcionar (pero no necesariamente la más fácil de implementar) es implementar su propia máquina virtual con su propia semántica sobre la JVM. (Tenga en cuenta que no es así como se hace realmente, esto es solo una intuición de por qué se puede hacer).

y puede ser implementado de manera eficiente sin él?

Realmente no.

Una explicación un poco más larga :

Tenga en cuenta que uno de los objetivos de Project Loom es introducir esta abstracción puramente como una biblioteca. Esto tiene tres ventajas:

  • Es mucho más fácil introducir una nueva biblioteca que cambiar el lenguaje de programación de Java.
  • Las bibliotecas pueden ser utilizadas inmediatamente por programas escritos en cada idioma en la JVM, mientras que una característica de lenguaje Java solo puede ser utilizada por programas Java.
  • Se puede implementar una biblioteca con la misma API que no utiliza las nuevas funciones de JVM, lo que le permitirá escribir código que se ejecute en JVM antiguas con una simple compilación (aunque con menos rendimiento).

Sin embargo, implementarlo como una biblioteca evita los astutos trucos del compilador convirtiendo las co-rutinas en otra cosa, porque no hay un compilador involucrado . Sin trucos inteligentes de compilación, obtener un buen rendimiento es mucho más difícil, por lo tanto, el "requisito" para el soporte de JVM.

Explicación más larga :

En general, todas las estructuras de control "potentes" usuales son equivalentes en un sentido computacional y pueden implementarse usando el uno al otro.

La más conocida de esas estructuras de flujo de control universal "poderosas" es la venerable GOTO , otra son las Continuaciones. Luego, hay Hilos y Corutinas, y uno que la gente no suele pensar, pero que también es equivalente a GOTO : Excepciones.

Una posibilidad diferente es una pila de llamadas re-ified, de modo que la pila de llamadas sea accesible como un objeto para el programador y pueda ser modificada y reescrita. (Muchos dialectos de Smalltalk hacen esto, por ejemplo, y también es similar a cómo se hace en C y ensamblaje).

Mientras tengas uno de esos, puedes tener todos esos, simplemente implementando uno encima del otro.

La JVM tiene dos de ellas: Excepciones y GOTO , pero GOTO en la JVM no es universal, es extremadamente limitada: solo funciona dentro de un único método. (Está destinado esencialmente solo para bucles.) Entonces, eso nos deja con Excepciones.

Entonces, esa es una posible respuesta a su pregunta: puede implementar rutinas conjuntas además de Excepciones.

Otra posibilidad es no utilizar el flujo de control de la JVM e implementar su propia pila.

Sin embargo, esa no suele ser la ruta que realmente se toma al implementar co-rutinas en la JVM. Lo más probable es que alguien que implemente rutinas conjuntas opte por usar Trampolines y parcialmente redefinir el contexto de ejecución como un objeto. Es decir, por ejemplo, cómo se implementan los generadores en C♯ en la CLI (no en la JVM, pero los desafíos son similares). Los generadores (que son básicamente semi-rutinas restringidas) en C♯ se implementan al levantar las variables locales del método en campos de un objeto de contexto y dividir el método en múltiples métodos en ese objeto en cada declaración de yield , convirtiéndolos en un máquina de estado, y enhebrar cuidadosamente todos los cambios de estado a través de los campos en el objeto de contexto. Y antes de que async / await apareciera como función de idioma, un programador inteligente implementó una programación asincrónica utilizando la misma maquinaria.

SIN EMBARGO , y eso es lo que probablemente mencionó el artículo que usted mencionó: toda esa maquinaria es costosa. Si implementa su propia pila o levanta el contexto de ejecución en un objeto separado, o compila todos sus métodos en un método gigante y usa GOTO todas partes (lo cual no es posible debido al límite de tamaño de los métodos), o use Excepciones como control -flow, al menos una de estas dos cosas será verdadera:

  • Sus convenciones de llamadas se vuelven incompatibles con el diseño de la pila de JVM que esperan otros idiomas, es decir, usted pierde interoperabilidad .
  • El compilador JIT no tiene idea de qué demonios está haciendo tu código, y se presenta con patrones de código byte, patrones de flujo de ejecución y patrones de uso (por ejemplo, arrojando y atrapando cantidades descomunales de excepciones) que no espera y no sabe cómo para optimizar, es decir, pierdes rendimiento .

Rich Hickey (el diseñador de Clojure) dijo una vez en una charla: "Tail Calls, Performance, Interop. Pick Two". Generalicé esto a lo que llamo Hickey''s Maxim : "Control avanzado: flujo, rendimiento, interoperabilidad. Elija dos".

De hecho, generalmente es difícil lograr incluso uno de interoperabilidad o rendimiento.

Además, su compilador se volverá más complejo.

Todo esto desaparece cuando la construcción está disponible de forma nativa en la JVM. Imagine, por ejemplo, si la JVM no tiene hilos. Entonces, cada implementación de lenguaje crearía su propia biblioteca Threading, que es difícil, compleja, lenta y no interopera con la biblioteca Threading de la implementación de otro idioma.

Un ejemplo reciente y real es lambdas: muchas implementaciones de lenguaje en la JVM tenían lambdas, por ejemplo, Scala. Entonces Java también agregó lambdas, pero como la JVM no es compatible con lambdas, debe codificarse de alguna manera, y la codificación que Oracle eligió era diferente a la que Scala había elegido anteriormente, lo que significaba que no se podía pasar una lambda de Java. a un método de Scala que espera una Function Scala. La solución en este caso fue que los desarrolladores de Scala volvieron a escribir completamente su codificación de lambdas para que fuera compatible con la codificación que Oracle había elegido. Esto realmente rompió la compatibilidad hacia atrás en algunos lugares.