Red de conocimiento informático - Problemas con los teléfonos móviles - Programación reactiva (responsive)

Programación reactiva (responsive)

Reactor y Rxjava son una implementación concreta del paradigma de programación reactiva, que se puede resumir como:

Como primer paso en la dirección de la programación reactiva, Microsoft creó Reactive en . NET ecosistema Extensiones (Rx) biblioteca. Luego, RxJava implementa programación reactiva en la JVM. Con el tiempo, la estandarización de Java surgió a través del trabajo en Reactive Streams, una especificación que define un conjunto de interfaces y reglas de interacción para bibliotecas reactivas en la JVM. Su interfaz se ha integrado en Java 9 bajo la clase principal Flow.

Además, Java 8 también presenta Stream, que está diseñado para manejar eficientemente flujos de datos (incluidos los tipos primitivos) a los que se puede acceder con poca o ninguna demora. Está basado en extracción, solo se puede usar una vez, carece de operaciones relacionadas con el tiempo y puede realizar cálculos paralelos, pero no puede especificar qué grupo de subprocesos usar. Pero no está diseñado para manejar operaciones retrasadas como operaciones de E/S. Las características que no admite son donde entran en juego las API reactivas como Reactor o RxJava.

Las API reactivas como Reactor o Rxjava también proporcionan operadores como Java 8 Stream, pero funcionan mejor con cualquier secuencia de flujos (no solo colecciones) y permiten definir una canalización de operaciones de transformación que se aplicarán a los datos que pasa a través de él, gracias a la conveniente API fluida y al uso de lambdas. Están diseñados para manejar operaciones sincrónicas o asincrónicas y le permiten almacenar en búfer, fusionar, unir o aplicar diversas transformaciones a los datos.

Primero piénselo, ¿por qué necesita una biblioteca de programación reactiva asíncrona? Las aplicaciones modernas pueden admitir un gran número de usuarios simultáneos y, aunque el hardware moderno sigue volviéndose más potente, el rendimiento del software moderno sigue siendo una cuestión crítica.

Se pueden aumentar las capacidades de un sistema de dos maneras:

Normalmente, los desarrolladores de Java escriben programas utilizando código de bloqueo. Esto funciona bien hasta que se produce un cuello de botella en el rendimiento, momento en el que es necesario introducir subprocesos adicionales. Sin embargo, esta expansión en la utilización de recursos puede introducir rápidamente problemas de contención y concurrencia.

Peor aún, puede provocar un desperdicio de recursos. Una vez que un programa implica algún retraso (especialmente E/S, como una solicitud de base de datos o una llamada de red), los recursos se desperdician porque el subproceso (o muchos subprocesos) ahora está inactivo, esperando datos.

Por lo que el método de paralelización no es una panacea, es necesario para obtener la funcionalidad completa del hardware.

El segundo método, que busca una mayor utilización de los recursos existentes, puede resolver el problema del desperdicio de recursos. Al escribir código asincrónico y sin bloqueo, puede usar los mismos recursos subyacentes para cambiar la ejecución a otra tarea activa y luego regresar al subproceso actual para continuar el procesamiento una vez que se complete el procesamiento asincrónico.

¿Pero cómo generar código asincrónico en JVM? Java proporciona dos modelos de programación asincrónica:

Sin embargo, los dos métodos anteriores tienen limitaciones.

En primer lugar, es difícil combinar varias devoluciones de llamada, lo que rápidamente hace que el código sea difícil de leer y mantener (llamado "Infierno de devolución de llamada"):

Considere el siguiente ejemplo: muestre los 5 favoritos de los usuarios principales en UI del usuario Información detallada de cada producto, si no existe, llame al servicio de recomendación para obtener 5. La implementación de esta función requiere tres soportes de servicio: uno es la interfaz para obtener la ID del producto que le gusta al usuario (userService). .getFavorites), y el segundo es obtener la interfaz de información de detalles del producto (favoriteService.getDetails), y el tercero es el servicio para recomendar productos y detalles del producto (suggestionService.getSuggestions El código para implementar la función anterior basado en). El modo de devolución de llamada es el siguiente:

Para implementar esta función como se indicó anteriormente, escribimos mucho código y usamos muchas devoluciones de llamada. Estos códigos son relativamente oscuros y hay duplicaciones de código que utilizamos. Reactor para lograr funciones equivalentes:

el futuro es mejor que la devolución de llamada, pero aunque CompletableFuture Se realizaron mejoras en Java 8, todavía no funcionan bien. Orquestar múltiples futuros juntos es factible pero no fácil. No admiten cálculos retrasados ​​(como la operación de aplazamiento en rxjava) ni manejo avanzado de errores, como el siguiente ejemplo. Considere otro ejemplo: primero obtenemos una lista de identificadores, luego obtenemos el nombre correspondiente y los datos estadísticos de acuerdo con el identificador, luego combinamos el nombre y los datos estadísticos correspondientes a cada identificador en nuevos datos y finalmente generamos los valores de todos. pares combinados, de la siguiente manera Usamos CompletableFuture para implementar esta función para garantizar que todo el proceso sea asincrónico y que el procesamiento correspondiente a cada identificación sea concurrente:

El propio Reactor proporciona más operadores listos para usar, usando El código para implementar la función anterior usando Reactor es el siguiente:

El código anterior escrito usando el método reactor es más conciso y más fácil de entender que usar CompletableFuture para lograr la misma función.

La componibilidad se refiere a la capacidad de orquestar múltiples tareas asincrónicas, utilizar los resultados de tareas anteriores como entrada para tareas posteriores o ejecutar múltiples tareas de forma fork-join.

La capacidad de orquestar tareas está estrechamente relacionada con la legibilidad y mantenibilidad del código. A medida que aumenta el número y la complejidad de las capas de procesos asincrónicos, se vuelve cada vez más difícil poder escribir y leer código. Como podemos ver, el modelo de devolución de llamada es simple, pero una de sus principales desventajas es que para un procesamiento complejo es necesario ejecutar una devolución de llamada a partir de una devolución de llamada, que a su vez está anidada dentro de otra devolución de llamada, y así sucesivamente. Ese lío se llama Callback Hell y, como puedes adivinar (o saber por experiencia), es muy difícil retroceder y razonar sobre dicho código.

Reactor proporciona ricas opciones de composición, donde el código refleja la organización del proceso abstracto y, en general, todo se mantiene en el mismo nivel (se minimiza el anidamiento).

Las materias primas pueden sufrir diversas transformaciones y otros pasos intermedios, o formar parte de una cadena de montaje más grande que reúne elementos intermedios. Si se produce un bloqueo en un punto de la línea de montaje, la estación de trabajo afectada puede enviar una señal aguas arriba para restringir el flujo descendente de materia prima.

Aunque la especificación de Reactive Streams no especifica operadores en absoluto, uno de los mejores valores añadidos de las bibliotecas reactivas como Reactor o rxjava es el rico conjunto de operadores que proporcionan. Estos van desde simples transformaciones y filtrado hasta complejas orquestaciones y manejo de errores.

En Reactor, cuando escribes una cadena de Publisher, los datos no se inician de forma predeterminada. En su lugar, crea descripciones abstractas de procesos asincrónicos (que pueden ayudar a la reutilización y la composición).

Las señales de propagación ascendentes también se utilizan para implementar contrapresión, que describimos en las líneas de montaje como señales de retroalimentación enviadas a la línea ascendente cuando una estación de trabajo procesa más lentamente que una estación de trabajo ascendente.

Esto convierte el modelo push en un modelo híbrido push-pull, donde si el upstream produce muchos elementos, el downstream puede extraer n elementos del upstream. Pero si el elemento no está listo, el elemento se producirá en sentido ascendente y luego los datos se enviarán en sentido descendente.