Distribución Freebie: Subprocesos sin frenos en Java. Telar del proyecto

¿Desea en los hilos de Java que no comen memoria como si no estuvieran en sí mismos y no se ralentizan? Buen crédito, y este problema responde a esta pregunta.


¡Explicamos el trabajo de Project Loom en cajas de pizza! Vamos!


Todo esto se elimina y se escribe específicamente para Habr .





Señales de llamada


¿Con qué frecuencia ve esta imagen en su servicio web: al principio todo estaba bien, luego vino a usted un millón de chinos, el servicio hizo un millón de hilos y se ahogó en el infierno?





¿Quieres una foto tan bonita?





En otras palabras, ¿quiere en los hilos de Java que no comen memoria pero no están en sí mismos y no se ralentizan? Buen crédito, y este problema responde a esta pregunta.


Estaremos comprometidos, de hecho, desempacando el nuevo marco . ¿Recuerdas cómo Wylsacom desempacó iPhones? Algunos ya no recuerdan a los críticos anteriores, pero ¿por qué? Debido a que Habr es una fortaleza dominante, y los vidos pagados son, lo siento, los rayos de la diarrea . En este post trataremos exclusivamente con el hardcore técnico.


Primero, dos minutos para los globos oculares, descargo de responsabilidad y otra basura, que hay que decir. Puedes omitirlo si eres demasiado vago.


En primer lugar, todo lo que se dice en el video son mis pensamientos personales, y no tienen nada que ver con el empleador o la gloriosa corporación Oracle, el gobierno mundial de lagartos y una maldita cosa en un mortero. Incluso escribo esto a las tres de la mañana para que quede claro que es mi iniciativa personal, mi basura personal. Todos los partidos son puramente aleatorios.


Pero hay otro objetivo extra. Hablamos constantemente de corutinas en Kotlin. Recientemente hubo entrevistas con Roma Elizarov , el dios de Corutin, y con Pasha Finkelstein , quien les escribirá backends. Pronto habrá una entrevista con Andrei Breslav, quien es el padre de Kotlin. Y en todas partes el proyecto Loom se menciona de una forma u otra, porque es un análogo de la rutina. Y si no sabe qué es Loom, puede quedarse atónito al leer estas entrevistas. Hay algunos tipos geniales, discuten cosas geniales. Y ahí estás, y no estás con ellos, idiota. Esto es muy estupido.


No hagas esto, lee lo que es Loom en este artículo, o mira este video más, explicaré todo.


Entonces, ¿cuál es la complicación? Hay un tipo así, Ron Presler.






El año pasado, fue a la lista de correo , dijo que los hilos en Java apestan y sugirió que ejecute el tiempo de ejecución y lo arregle. Y todos se reirían de él y le arrojarían piedras, mierda, si no fuera por el hecho de que escribió Quasar antes, y esto es realmente genial. Puedes maldecir en Quasar durante mucho tiempo, pero parece estar allí y funciona, y en el panorama general de todo esto es más probable que sea un logro.


Hay un montón de govnokodery que no hacen nada, solo dicen. Bueno, hazlo bien, soy igual. O hay personas que parecen ser ingenieros geniales, pero en general en el inconsciente, dicen lo siguiente: "En Java, necesitas mejorar los hilos". ¿Qué mejorar? ¿Qué son los hilos?


La gente generalmente es demasiado perezosa para pensar.


Como en una broma:
Petka y Vasily Ivanovich vuelan en un avión.
Vasily Ivanovich pregunta: - Petka, ¿dispositivos?
Petka responde: - 200!
Vasily Ivanovich: - ¿Y qué hay de 200?
Petka: - ¿Qué pasa con los electrodomésticos?


Te contaré una historia. Estuve en Ucrania esta primavera, volamos desde Bielorrusia (entiendes por qué es imposible directamente desde San Petersburgo). Y en la aduana nos quedamos sentados unas dos horas, nada menos. Los funcionarios de aduanas son muy amables, con toda seriedad preguntaron si Java es una tecnología obsoleta. Cerca se sentaron personas que vuelan al mismo konf. Y soy un tipo de orador, tengo que hacer una pausa, pararme y, como era de esperar, hablar descaradamente de cosas que no uso en absoluto. Y en el camino habló sobre la distribución JDK llamada Liberica, este es un JDK para Raspberry Pi.


Y que piensas Ni siquiera pasan seis meses antes de que la gente golpee mi carrito y diga que, miren, hemos puesto una solución en Liber en el producto, y ya tengo un informe sobre esto en la Bielorrusia jfuture.by konf. Este es el enfoque. Este no es un pésimo evangelista, sino un tipo, un ingeniero normal.


Por cierto, pronto tendremos una conferencia Joker 2018 , que incluirá tanto a Andrei Breslav (obviamente hurgando en las rutinas) como a Pasha Finkelshtein , y a Josh Long se les puede preguntar sobre el apoyo de Spring para Loom. Bueno, y un montón de expertos destacados, ¡vamos!

Y ahora, volviendo a los hilos. Las personas intentan dibujar un pensamiento a través de sus dos neuronas que se han degradado, se enroscan en el puño y murmuran: "Los hilos no son así en Java, los hilos no son así en Java". Dispositivos! Que electrodomésticos Esto es generalmente el infierno.


Y aquí viene Presler, un tipo normal, no degradado, y al principio hace una descripción sensata. Un año después, aserrando una demo de trabajo. Dije todo esto para que comprenda que una descripción normal de los problemas, la documentación normal es un heroísmo de un tipo especial. Y la demostración es generalmente espacio. Esta es la primera persona que realmente ha hecho algo en esta dirección. El más lo necesita.


Junto con la demostración, Presler habló en la conferencia y lanzó este video:



De hecho, todo este artículo es una revisión de lo que se dijo allí. No pretendo en absoluto la singularidad de este material, todo lo que está en este artículo fue inventado por Ron.


La discusión trata sobre tres temas dolorosos:


  • Continuaciones
  • Fibras
  • La cola llama

Probablemente, estaba tan asqueado al aserrar a Quasar y pelear con sus fallas que no hay fuerza: debes empujarlo al tiempo de ejecución.


Fue hace un año, y desde entonces han estado aserrando un prototipo. Algunos ya han perdido la esperanza de que algún día veremos una demostración, pero hace un mes la dieron a luz y mostraron lo que se ve en este tweet .






Los tres temas dolorosos en esta demostración están directamente en el código, o al menos moralmente presentes. Bueno, sí, todavía no han dominado las llamadas de cola, pero quieren hacerlo.


Problema


Los usuarios descontentos, los desarrolladores de aplicaciones, cuando hacen una API, se ven obligados a elegir entre dos sillas. Los picos están integrados en una silla, las flores crecen en la otra. Y ni uno ni el otro nos conviene.






Por ejemplo, si escribe un servicio que funciona sincrónicamente, funciona muy bien con código heredado, es fácil de depurar y monitorear el rendimiento. Surgirán problemas con el ancho de banda y la escalabilidad. Solo porque la cantidad de subprocesos que ahora puede ejecutar en una pieza simple de hardware, en hardware básico, bueno, digamos, dos mil. Esto es mucho menor que la cantidad de conexiones que podrían abrirse a este servidor. Lo que desde el punto de vista del netcode puede ser casi interminable.


(Bueno, sí, tiene algo que ver con el hecho de que los sockets en Java están organizados de forma tonta, pero este es un tema para otra conversación)


Imagina que estás escribiendo algún tipo de MMO.






Por ejemplo, durante la Guerra del Norte en EVE Online, dos mil cuatrocientos pilotos se reunieron en un punto en el espacio, cada uno de ellos, condicionalmente, si estuviera escrito en Java, no sería un hilo, sino varios. Y el piloto, por supuesto, es una lógica empresarial compleja, y no la emisión de ningún HTML que pueda descartarse a mano en una lupa.


El tiempo de respuesta en esa batalla fue tan largo que el jugador tuvo que esperar unos minutos para esperar el tiro. Hasta donde sé, el PCCh específicamente para esa batalla arrojó los enormes recursos de hardware de su clúster.


Aunque, probablemente estoy citando a EVE como un ejemplo en vano, porque, por lo que entiendo, todo está escrito en Python, y en Python con subprocesos múltiples es aún peor que el nuestro, y podemos considerar una pobre competencia de características del lenguaje. Pero entonces el ejemplo es claro y con imágenes.


Si está interesado en el tema de la OMI en general y en la historia de la "Guerra del Norte" en particular, recientemente apareció un video muy bueno sobre este tema en el canal Bulzhat (lo que sea que ese nombre signifique), mire desde mi marca de tiempo.



Volvemos al tema.


Por otro lado, puede usar algún tipo de marco asincrónico. Es escalable Pero inmediatamente caeremos en una depuración muy difícil, un perfil complicado del rendimiento, no podremos integrarlo a la perfección con el legado, tendrá que reescribir muchas cosas, envolverlas en envoltorios abominables y, en general, sentir que fuimos violados. Varias veces seguidas. Durante días, de hecho, todo el tiempo que escribamos esto, tendrá que sentirse así.


Le pregunté al experto, el famoso académico Escobar, qué piensa sobre esto:






Que hacer Los llamados faybers corren al rescate.


En el caso general, las fibras son hilos tan livianos que también hurgan en el espacio de direcciones (porque los milagros no suceden). Pero a diferencia de los hilos comunes, no utilizan la multitarea preventiva, sino la multitarea cooperativa. Leer más en Wikipedia .


Fiber puede darse cuenta de las ventajas de la programación síncrona y asíncrona. Como resultado, la utilización de hierro aumenta y utilizamos menos servidores en el clúster para la misma tarea. Bueno, en nuestro bolsillo por esto obtenemos lavanda. Babos Lave Dinero Bueno, entiendes el punto. Para el servidor guardado.


En la encrucijada


Lo primero que quiero discutir. La gente no entiende la diferencia entre las continuaciones y la fibra.
¡Ahora habrá una iluminación de culto!


Anunciaremos el hecho: Continuación y Fibra son dos cosas diferentes.


Continuaciones


Las fibras se construyen sobre una mecánica llamada Continuaciones.


Las continuaciones (más precisamente, continuaciones delimitadas) es un tipo de cálculo, ejecución, una parte de un programa que puede quedarse dormido, luego despertarse y continuar la ejecución desde el lugar donde se quedó dormido. A veces incluso se puede clonar o serializar, incluso mientras está durmiendo.


Usaré la palabra "continuación", y no "continuación" (como está escrito en Wikipedia), porque todos nos comunicamos en inglés . Usando la terminología rusa normal, uno puede llegar fácilmente a una situación en la que la diferencia entre el término ruso e inglés se vuelve demasiado grande y nadie más entiende el significado de lo que se dijo.


A veces también usaré la palabra “desplazar” en lugar de la versión en inglés de “ceder”. Solo la palabra "ceder": es algo realmente desagradable. Por lo tanto, habrá "desplazamiento".


Entonces aquí. Es muy importante que no haya competencia dentro del continuo. Es en sí mismo la primitiva mínima de este proceso.


Puede pensar en la continuación como un Runnable , dentro del cual puede llamar al método pause() . Está dentro y directamente, porque nuestra multitarea es cooperativa. Y luego puede ejecutarlo nuevamente, y en lugar de volver a calcular todo, continuará desde donde lo dejó. Ese tipo de magia. Volveremos a la magia.


Dónde obtener una demostración con continuaciones de trabajo: discutiremos al final. Ahora hablemos de lo que hay allí.


La clase de continuación en sí se encuentra en java.base, todos los enlaces estarán en la descripción. ( src/java.base/share/classes/java/lang/Continuation.java ). Pero esta clase es muy grande, voluminosa, por lo que tiene sentido mirar solo algún tipo de compresión.


 public class Continuation implements Runnable { public Continuation(ContinuationScope scope, Runnable body); public final void run(); public static void yield(ContinuationScope scope); public boolean isDone(); protected void onPinned(Reason reason) { throw new IllegalStateException("Pinned: " + reason); } } 

Tenga en cuenta que, de hecho, este archivo cambia constantemente. Por ejemplo, a partir del día anterior, la continuación no implementó la interfaz Runnable . Trata esto como una especie de bosquejo.


Echa un vistazo al constructor. body (este es el código que está intentando ejecutar y el scope ) es un tipo de skop que le permite anidar continuaciones en continuaciones.


En consecuencia, puede preprogramar este código hasta el final con el método de run , o suplantarlo con una matriz específica utilizando el método de yield (la matriz se necesita aquí para algo como reenviar acciones a controladores anidados, pero no nos importan los usuarios). Puede preguntar utilizando el método isDone si todo se completa hasta el final.


Y por razones dictadas únicamente por las necesidades de la implementación actual (pero muy probablemente, también se incluirá en el lanzamiento), no siempre es posible obtener yield . Por ejemplo, si dentro de la continuación tuvimos una transición al código nativo y apareció un marco nativo en la pila, entonces es imposible quedarse atascado. Esto también sucederá si intenta exprimirse mientras se toma un monitor nativo, como un método sincronizado, dentro del cuerpo del continuo. Por defecto, cuando intentas fingir esto, se produce una excepción ... pero las fibras construidas sobre las continuaciones sobrecargan este método y hacen otra cosa. Esto será un poco más tarde.


Puede usar esto aproximadamente de la siguiente manera:


 Continuation cont = new Continuation(SCOPE, () -> { while (true) { System.out.println("before"); Continuation.yield(SCOPE); System.out.println("after"); } }); while (!cont.isDone()) { cont.run(); } 

Este es un ejemplo de la presentación de Presler. Nuevamente, este no es un código "trivial", es algún tipo de bosquejo.


Este es un bosquejo de lo que estamos haciendo continuación, en el medio de esta continuación estamos desplazados y luego, en un ciclo interminable, preguntamos si la continuación funcionó hasta el final y si debería continuarse.


Pero, en general, no se pretende que los programadores de aplicaciones comunes se relacionen con esta API. Está destinado a los creadores de marcos de sistemas. Los marcos formadores de sistemas como Spring Framework adoptarán de inmediato esta característica tan pronto como salga. Ya veras. Considera esto como una predicción. Una predicción tan ligera, porque todo es bastante obvio aquí. Todos los datos para la predicción son. Esta característica es demasiado importante para no adaptarse. Por lo tanto, no hay necesidad de preocuparse de antemano de que alguien lo torturará con la codificación de esta forma. Bueno, si eres un desarrollador de Spring, entonces sabías lo que estabas haciendo.


Y ahora, además de las continuaciones, se construyen fibras.


Fibras


Entonces, lo que en nuestro caso significa fibra.

Este es un tipo de abstracción, que es:


  • Subprocesos ligeros procesados ​​en la propia JVM y no en el sistema operativo;
  • Con gastos generales extremadamente bajos para crear, mantener la vida, cambiar tareas;
  • Que se puede ejecutar millones de veces.

Muchas tecnologías están tratando de fabricar fibra de una forma u otra. Por ejemplo, en Kotlin hay corutinas implementadas en la generación de bytecode muy inteligente. MUY INTELIGENTE Pero el tiempo de ejecución es un mejor lugar para implementar tales cosas.


Como mínimo, la JVM ya sabe cómo manejar bien los subprocesos, y todo lo que necesitamos hacer es optimizar el proceso de codificación para subprocesos múltiples. Puede usar API asincrónicas, pero esto difícilmente se puede llamar "simplificación": incluso usar cosas como Reactor , Spring Project Reactor, que permiten escribir código aparentemente lineal, no ayudará mucho si necesita depurar problemas complejos.


Entonces fibra.


La fibra consta de dos componentes. Esto es:


  • Continuación
  • Programador

Eso es:


  • Continuación
  • Planificador





Puedes decidir quién es el planificador aquí. Creo que el planificador aquí es Jay.


  • Fiber envuelve el código que desea ejecutar en una continuación
  • El programador los lanza en un grupo de hilos portadores

Los llamaré hilos de soporte.







El prototipo actual usa java.util.concurrent.Executor , y el programador ForkJoinPool . Lo tenemos todo En el futuro, algo más inteligente puede aparecer allí, pero por ahora, así.


¿Cómo se comporta la continuación?


  • Se desplaza (rendimiento) cuando se produce un bloqueo (por ejemplo, en IO);
  • Continúa cuando está listo para continuar (por ejemplo, la operación de E / S se ha completado y puede continuar).

Estado actual de las obras:


  • El foco principal en filosofía, conceptos;
  • La API no está arreglada, es "para mostrar". Este es un prototipo de investigación;
  • Hay un prototipo de trabajo codificado listo para usar de la clase java.lang.Fiber .

Será discutido


Lo que ya se ha aserrado en la fibra:


  • Ejecuta un inicio de tarea;
  • Estacionamiento de vehículos sin estacionamiento en un transportista;
  • A la espera de la finalización de la fibra.

Diagrama de circuito


 mount(); try { cont.run(); } finally () { unmount(); } 

  • Podemos montar una fibra en un portador de hilo;
  • Luego ejecuta la continuación;
  • Y espere hasta que ella sea desplazada o honestamente se detenga;
  • Al final, siempre dejamos el hilo.

Este pseudocódigo se ejecutará en el ForkJoinPool o en algún otro (que eventualmente estará en la versión final).


Uso en la realidad


 Fiber f = Fiber.execute( () -> { System.out.println("Good Morning!"); readLock.lock(); try { System.out.println("Good Afternoon"); } finally { readLock.unlock(); } System.out.println("Good Night"); }); 

Mira, estamos creando una fibra en la que:


  • bienvenidos a todos;
  • estamos bloqueando el loke reentrante;
  • a su regreso, felicidades por su almuerzo;
  • finalmente suelte la cerradura;
  • y decir adios.

Todo es muy sencillo.


No causamos hacinamiento directamente. Project Loom mismo sabe que cuando se readLock.lock(); él debería intervenir e implícitamente hacer represión. El usuario no ve esto, pero sucede allí.


¡Pilas, pilas por todas partes!


Vamos a demostrar lo que está sucediendo usando la pila de pizza como ejemplo.


Al principio, el subproceso de la portadora está en estado de espera y no sucede nada.





Arriba de la pila en la parte superior, recordar.


Luego se programó la ejecución de la fibra y la tarea de fibra comenzó a ejecutarse.





Dentro de sí mismo, obviamente lanza una continuación, en la que el código real ya está ubicado.





Desde el punto de vista del usuario, todavía no hemos lanzado nada aquí.


Ese es solo el primer fotograma del código de usuario que apareció en la pila, y está marcado en púrpura.


Además, el código se ejecuta, se ejecuta, en algún momento la tarea está tratando de capturar el bloqueo y bloquearlo, lo que conduce a un desplazamiento automático.





Todo lo que está en la pila de continuación se almacena en un cierto lugar mágico. Y desaparece





Como puede ver, la secuencia vuelve a la fibra, a las instrucciones que siguen a Continuation.run . Y este es el final del código de fibra.


La tarea de fibra finaliza, el soporte de medios está esperando un nuevo trabajo.





La fibra está estacionada, en algún lugar se encuentra, el continuo está completamente desplazado.


Tarde o temprano, llega el momento en que el propietario de la cerradura la libera.
Esto lleva al hecho de que la fibra, que estaba esperando el desbloqueo, está desempacada. La tarea de esta fibra comienza de nuevo.


  • Reentrantlock.unlock
  • Locksupport.unpark
  • Fiber.unpark
  • ForkJoinPool.execute

Y rápidamente volvemos a la pila, que era recientemente.





Además, el hilo portador puede ser completamente diferente. ¡Y eso tiene sentido!


Ejecute la continuación nuevamente.





¡Y aquí viene la MAGIA! La pila se restaura y la ejecución continúa con las instrucciones después de Continuation.yield .





Salimos del bloqueo recién estacionado y comenzamos a ejecutar todo el código restante en la continuación:





La tarea del usuario finaliza y el control vuelve a la tarea de fibra inmediatamente después de la instrucción de continuación.





Al mismo tiempo, finaliza la ejecución de la fibra, y nuevamente nos encontramos en modo de espera.





El próximo lanzamiento de la fibra nuevamente inicia todo el ciclo de renacimientos descrito anteriormente.





Ejemplos en vivo


¿Y quién dijo que todo esto funciona? ¿Se trata de un par de microbenchmarks escritos durante la noche?


Como ejemplo de la operación de los petardos, los Oraklovites escribieron un pequeño servidor web y lo alimentaron con solicitudes para que se ahogara. Luego se transfirieron a fibra. El servidor dejó de atragantarse, y de esto concluimos que las fibras funcionan.


No tengo el código exacto para este servidor, pero si esta publicación recibe suficientes me gusta y comentarios, intentaré escribir un ejemplo yo mismo y crear gráficos reales.


Los problemas


¿Hay algún problema aquí? Si por supuesto! Toda la historia con faybers es una historia sobre problemas continuos y compensaciones.


Problemas filosóficos


  • ¿Necesitamos reinventar hilos?
  • ¿Debería todo el código existente funcionar correctamente dentro de la fibra?

El prototipo actual funciona con limitaciones. Lo que puede salir al mercado, aunque no quisiera. Aún así, OpenJDK es una cosa que respeta la compatibilidad infinita.


¿Cuáles son las limitaciones técnicas? Las limitaciones más obvias son 2 piezas.


El problema es una vez: no puede suplantar marcos nativos


 PrivilegedAction<Void> pa = () -> { readLock.lock(); // may park/yield try { // } finally { readLock.unlock(); } return null; } AccessController.doPrivileged(pa); //native method 

Aquí doPrivileged llama al método nativo.


Llama a doPrivileged , salta de la VM, aparece un marco nativo en su pila, después de lo cual intenta estacionar en la readLock.lock() . Y en ese momento, el hilo portador se manchará hasta que se desbloquee. Es decir, el hilo desaparece. En este caso, los hilos portadores pueden terminar y, en general, esto rompe toda la idea de la fibra.


La forma de resolver esto ya se conoce, y hay discusiones en curso sobre esto.


Problema dos: bloques sincronizados


Esto es basura mucho más seria


 synchronized (object) { //may park object.wait(); //may park } 

 synchronized (object) { //may park socket.getInputStream().read(); //may park } 

En caso de captura de monitor en la fibra, los hilos portadores también patean.


Está claro que en un código completamente nuevo puede cambiar los monitores a bloqueos directos, en lugar de esperar + notificar que puede usar objetos de condición, pero ¿qué hacer con el legado? Esto es un problema


Hilo API? Thread.currentThread ()? Hilo locales?


En el prototipo actual, Thread and Fiber han creado una superclase común llamada Strand .


Esto le permite transferir la API de la manera más mínima.
Qué hacer a continuación, como siempre en este proyecto, es una pregunta.


¿Qué está sucediendo con Thread API ahora?


  • El primer uso de Thread.currentThread() en una fibra crea una especie de hilo de sombra, Shadow Thread;
  • desde el punto de vista del sistema, este es un hilo "inédito" y no contiene metainformación de VM;
  • ST intenta emular todo lo que puede;
  • pero tienes que entender que la antigua API tiene mucha basura;
  • más específicamente, Shadow Thread implementa la API Thread para todo, excepto stop , suspend , resume y manejar excepciones no detectadas.

¿Qué hacer con los hilos locales?


  • ahora los locales de subprocesos simplemente se convierten en locales de fibra;
  • Hay muchos problemas con esto, todo esto se está discutiendo;
  • se discute especialmente un conjunto de usos;
  • Los hilos se han usado históricamente de manera correcta e incorrecta (los que usan incorrectamente todavía esperan algo, y no pueden decepcionarlos por completo);
  • en general, esto crea una amplia gama de aplicaciones:
    • Alto nivel: caché de conexiones o contraseñas en el contenedor;
    • Bajo nivel: procesador en bibliotecas del sistema.

¿Cuánto come todo?





Hilo:


  • Pila: 1 MB y 16 KB en estructuras de datos del núcleo;
  • Por instancia de subproceso: 2300 bytes, incluida la metainformación de VM.

Fibra:


  • Pila de continuación: de cientos de bytes a kilobytes;
  • Por instancia de fibra: 200-240 bytes.

¡La diferencia es enorme!
Y eso es exactamente lo que permite que millones de personas se enciendan.


¿Qué puede aparcar?


Está claro que lo más mágico es el estacionamiento automático cuando ocurren algunos eventos. ¿Qué se admite actualmente?


  • Thread.sleep, únete;
  • java.util.concurrent y LockSupport.lock;
  • IO: red en sockets (lectura, escritura, conexión, aceptación), archivos, tuberías;
  • Todo esto está inacabado, pero la luz en el túnel es visible.

Comunicación entre fibra


Otra pregunta que todos hacen es: cómo intercambiar información de manera competitiva entre las fibras.


  • El prototipo actual inicia tareas en Runnable , se puede convertir a CompletableFuture , si por alguna razón lo necesita;
  • java.util.concurrent "simplemente funciona". Puedes buscar todo de forma estándar;
  • puede haber nuevas API para subprocesos múltiples, pero esto no es exacto;
  • un montón de pequeñas preguntas como "¿deberían las fibras devolver valores?"; todo se discute, no están en el prototipo.

¿Cómo se implementan las continuaciones en el prototipo?


Los requisitos obvios se imponen en la continuación: debe usar la menor cantidad de RAM posible y debe cambiar entre ellos lo más rápido posible. De lo contrario, no funcionará mantenerlos en millones. La tarea principal aquí es de alguna manera no hacer una copia completa de la pila para cada parque de estacionamiento. ¡Y hay tal esquema! Tratemos de explicar esto en las fotos.


La mejor manera sería, por supuesto, simplemente poner todas las pilas en una cadera de Java y usarlas directamente. Pero no está claro cómo codificar ahora, por lo que el prototipo usa la copia. Pero copiando con un pequeño pero importante truco.


Tenemos dos sillas ... quiero decir, dos pilas. Dos matrices de Java en la cadera. Uno es una matriz de objetos, donde almacenaremos referencias a objetos. El segundo es primitivo (por ejemplo, íntimo), que se encargará de todo lo demás.





Ahora estamos en un estado donde la continuación está por realizarse por primera vez.


run llama a un método interno llamado enter :





Y luego se ejecuta el código de usuario, hasta el primer desplazamiento.





En este punto, se realiza una llamada de VM, que se freeze . En este prototipo, esto se hace directamente físicamente, usando una copia.





Comenzamos el proceso de copiar secuencialmente fotogramas de la pila nativa a java hip.





Es necesario verificar si los monitores se mantienen allí o si se usa el código nativo, o algo más que realmente no nos permitirá seguir trabajando.





Y si todo está bien, copiamos primero en una matriz primitiva:





Luego aislamos las referencias a los objetos y los guardamos en la matriz de objetos:





En realidad, ¡dos tés para todos los que leen a este lugar!


Además, continuamos este procedimiento para todos los demás elementos de la pila nativa.





¡Hurra! Copiamos todo al nido en la cadera. Puede saltar con seguridad al lugar de la llamada sin temor a que hayamos perdido algo. Todo está a la moda.





Ahora, tarde o temprano, el código de llamada volverá a llamar a nuestra continuación. Y ella debe continuar desde el lugar donde la dejaron la última vez. Esta es nuestra tarea.





Verificando si la continuación se estaba ejecutando, dice que sí, se estaba ejecutando. Por lo tanto, debe llamar a VM, limpiar un poco de espacio en la pila y llamar a la función interna de thaw VM. "Descongelar" se traduce al ruso como "descongelar", "descongelar", lo que suena bastante lógico. Es necesario descongelar fotogramas de la pila de continuación en nuestra pila nativa principal.


No estoy seguro de que el té descongelado sea bastante claro. La mala abstracción es como un gatito con una puerta. Pero esto hará por nosotros.





Hacemos copias bastante obvias.


Primero con una matriz primitiva:





Luego desde el enlace:





Necesita parchear un poco la copia para obtener la pila correcta:





Repita la obscenidad para todos los cuadros:





Ahora puede volver a yield y continuar como si nada hubiera pasado.





El problema es que una copia completa de la pila no es lo que nos gustaría tener. Es muy inhibitorio. Todo esto es el aislamiento de enlaces, verifica la fijación, no es rápido. Y lo más importante: ¡todo esto depende linealmente del tamaño de la pila! En una palabra, infierno. No hay necesidad de hacer esto.


En cambio, tenemos otra idea: copia perezosa.


Volvamos al lugar donde ya tenemos una continuación congelada.





Continuamos el proceso como antes:





De la misma manera que antes, limpiamos el lugar en la pila nativa:





Pero no estamos copiando todo en una fila, sino solo uno o un par de cuadros:





Ahora el hack. Debe parchear la dirección de retorno del método C para que apunte a una cierta barrera de retorno:





Ahora puede volver a yield con seguridad:





Lo que a su vez conducirá a una llamada al código de usuario en el método C :





Ahora imagine que C quiere volver al código que lo llamó. ¡Pero su invocador es B , y no está en la pila! Por lo tanto, cuando intenta regresar, irá a la dirección de retorno, y esta dirección es ahora la barrera de retorno. Y, ya sabes, esto nuevamente generará una llamada de thaw :





Y thaw descongelará el siguiente cuadro en la pila de continuación, y esto es B :





De hecho, lo copiamos perezosamente, a pedido.


Luego, soltamos B de la pila de continuación y establecemos la barrera nuevamente (la barrera debe establecerse porque queda algo en la pila de continuación). Y así sucesivamente.





Pero suponga que B no va a volver al código de llamada, sino que primero llama a otro método D Y este nuevo método también quiere ser desplazado.





En este caso, cuando llegue el momento de freeze , necesitaremos copiar solo la parte superior de la pila nativa en la pila de continuación:





Por lo tanto, la cantidad de trabajo realizado no depende linealmente del tamaño de la pila. Depende linealmente solo del número de cuadros que realmente usamos en el trabajo.


Lo que queda


Los desarrolladores tienen en cuenta algunas características, pero no entraron en el prototipo.


  • Serialización y clonación. La capacidad de continuar en otra máquina, en otro momento, etc.
  • JVM TI y depuración, como si fueran hilos normales. Si está bloqueado al leer el zócalo, entonces no verá un salto hermoso del rendimiento, en el prototipo, el hilo simplemente se bloqueará, como cualquier otro hilo ordinario.
  • La recursión de la cola ni siquiera fue tocada.

Próximos pasos:


  • Hacer una API humana;
  • Agregue todas las características que faltan;
  • Mejora el rendimiento.

Donde conseguir


El prototipo se realiza como un brunch en el repositorio de OpenJDK. Puede descargar el prototipo aquí cambiando a las fibers brunch.


Se hace así:


 $ hg clone http://hg.openjdk.java.net/loom/loom $ cd loom $ hg update -r fibers $ sh configure $ make images 

, OpenJDK. , -, - , .





-, C++ GNU . , Windows. , VirtualBox Ubuntu , Cygwin msys64. , msys , Cygwin.


, , , .


- , mercurial extension fsmonitor. , , hg help -e fsmonitor .
~/.hgrc :


 [fsmonitor] mode = on 

- . -, cp -R ./loom ./loom-backup .


, . , Java- , .


sh configure - . , Ubuntu, Autoconf ( sudo apt-get install autoconf ). — OpenJDK Ubuntu, , . Windows , .


, , hg diff --stat -r default:fibers .


, , , .


Conclusión


«, ». «», . «Loom» — « ». Project Loom .


, . , «» , , — , , , — .


, , XIX , .




. -, .


, . IDE .


, , , « », «», « » .


? . .


Gracias

Source: https://habr.com/ru/post/es422519/


All Articles