Programacion Paralela Con Java Fork-Join Ejemplo Practico

Descargar como docx, pdf o txt
Descargar como docx, pdf o txt
Está en la página 1de 10

ALGORITMOS PARALELOS

Programación paralela en Java con


el framework Fork/Join y su
aplicación en el mundo real

Introducción

El framework Fork/Join (Introducido en Java 7). El uso de este framework


nos permite usar todo el poder que hay en nuestras computadoras
utilizando todos los procesadores disponibles en el momento, con el fin de
llegar a un resultado mucho más rápidamente. De igual manera podemos
encontrarlo en la programación del lado cliente en la librería RxJs.
Principalmente lo que hace este framework es dividir tareas en subtareas y
ejecutarlas paralelamente con el fin de distribuir el trabajo.

No es muy común ver el uso de este framework explícitamente en


repositorios de código. Más bien, lo encontramos escrito internamente en
otras librerías de programación concurrente ó paralela como lo son los
CompletableFuture o la Stream API, ambas originarias de Java 8. Es
importante acotar que repasaremos el concepto de este framework mediante
un ejemplo práctico, para que sea más didáctico y se vea su aplicación en el
mundo real.
Qué es el Framework Fork/Join

Divide y vencerás
Divide y vencerás. Una frase muy popular en la política e históricamente en las
guerras. Si llevamos esta frase a la computación de tareas, podemos asimilarlo
como si estuviésemos atacando un ejército de 500 combatientes. Si
dividiésemos esa cantidad en grupos más pequeños de combatientes, será
mucho más fácil ganarles a cada grupo pequeño que a un ejército entero.

Figura: #1 (diagrama)

Gráfica de cómo se comportaría la computación en paralelo con el framework


Fork/Join
El framework Fork/Join implementa la interface ExecutorService, cuya
función de esta es permitirnos construir una tarea para que pueda ser
procesada asíncronamente por un hilo. El framework Fork/Join posee un pool
de hilos (threads), que es como un conjunto de procesadores disponibles
en la Java Virtual Machine. Cada uno de estos hilos actuará como una “Double-
Ended Queue” que es una cola doblemente terminada o dique, es decir, es
una estructura de datos lineal que permite insertar y eliminar elementos por
ambos extremos, podría verse como un mecanismo que permite aunar en una
única estructura las funcionalidades de las pilas (estructuras LIFO) y las colas
(estructuras FIFO), en otras palabras, estas estructuras (pilas y colas) podrían
implementarse fácilmente con una dique, para almacenar tareas y llevarlas a
su ejecución.

Ejercicio
Fundamentalmente se ha creado una problemática que puede ser muy
recurrente en el día a día de un programador. En esta problemática podemos
encontrar una lista de dispositivos; cada dispositivo con atributos como
batería, modelo y marca. Nuestro principal objetivo es encontrar el
porcentaje de batería promedio de todos los dispositivos registrados
en una lista.

Modelo para el dispositivo

Es bien entonces que tendremos una lista de n dispositivos, la cual


recorreremos para obtener la batería de cada dispositivo y encontrar el
promedio del porcentaje de batería entre todos estos.

List<Dispositivo> dispositivos;
Especificaciones técnicas de la máquina donde
se correrán las pruebas

Photo por Dibbendu Koley on Unsplash

Para la ejecución de nuestra pruebas en paralelo utilizaremos un procesador


con 6 núcleos y 12 hilos ~3.0GHz.

Se menciona las características del procesador para que se tenga en cuenta a


la hora de las conclusiones, ya que el tiempo de ejecución será una pieza
primordial para entender la rapidez de nuestros algoritmos en paralelo.

¡Ahora se procede a la solución del problema planteado! Se Hacen 3


diferentes algoritmos para encontrar el promedio del porcentaje de batería
en nuestra lista de dispositivos y luego veremos cómo se comporta cada uno.
Entre ellos estará la implementación con el framework Fork/Join.

Práctica con más tendencia: utilización de la


Stream API
Esta sería la solución más óptima al día de hoy. En un bloque de código de
no más de 5 líneas estamos “mapeando” el atributo de la batería, para luego
obtener su promedio mediante programación funcional a través de esta
hermosa librería. Es importante decir que al tratar la lista de dispositivos
como un flujo de datos paralelos (método parallelStream), se está
haciendo uso del framework Fork/Join internamente.

Figura: #2

Librería Stream de Java 8 para obtener el promedio de batería de una lista de


dispositivos

Lo que hace internamente esta librería Stream de Java sería lo siguiente:

Figura: #3

La Imagen les muestra cómo funcionaría el método parallelStream() de la


librería Stream de Java 8 para procesar datos en paralelo. Simplemente toma
la lista de dispositivos y pone las tareas en cada hilo (Thread) y al final obtiene
el resultado.
Práctica tradicional: utilizar un foreach
secuencial
A pesar de que la programación funcional ha venido cogiendo fuerza y ha
venido caminando a pasos agigantados, sigue siendo popular o veremos con
mucha frecuencia en Sistemas Legacy la utilización de algoritmos secuenciales.

Figura: #4

Algoritmo secuencial para obtener el promedio de batería de una lista de


dispositivos

Figura: #5

Esta imagen de referencia muestra como funcionaría el algoritmo de obtener el


promedio corriendo en un único hilo (Thread).

Práctica objetivo: utilización del framework


Fork/Join

En primer lugar, se crea una clase llamada SumatoriaBateriaForkJoin. En


esta clase se implementará la lógica que nos permitirá decidir si debemos
dividir nuestra tarea de sumar las baterías de los dispositivos en listas más
pequeñas o sumar la lista actual que se posee. Además, en esta clase
extenderemos de la clase RecursiveAction que correspondería a una tarea
ForkJoin.

Figura: #6

Si nos centramos en el método compute(), podemos ver la referencia del


siguiente ejemplo propuesto en la documentación de Java. En este método
pondremos la condición de que si la lista es suficientemente grande, entonces
que la divida en dos nodos cada uno con la mitad de elementos y así
sucesivamente hasta que tenga sublistas más pequeñas que cumplan con el
límite establecido por el programador en la variable threshold.

if(son pocos elementos en la lista)


sumar las baterías de los elementos en la lista
else
dividir mi trabajo en "sublistas" más pequeñas
invocar el trabajo de las "sublistas" más pequeñas y esperar
el resultado

En este caso, tenemos un límite (theshold) de procesamiento de 500,000


elementos por cada subtarea. Si la lista tiene un número de elementos mayor
al límite, los va a dividir en subtareas hasta cumplir esa condición (fork). Una
vez se cumpla la condición, hará la sumatoria para esos elementos y
finalmente reunirá los resultados de todas las subtareas en un único resultado
(join). Ver el siguiente código Java:
Figura: #7

Es entonces, como vemos en el código anterior, de que si la cantidad de


elementos a procesar es mayor al límite (threshold), cogería el camino del
else, creando dos nuevos nodos izquierdo y derecho, y así sucesivamente
como se explicó anteriormente hasta tener sublistas pequeñas y poder hacer la
suma paralelamente en los diferentes nodos creados. Ver la figura #1.

En segundo lugar, crearemos un pool de hilos con los procesadores


disponibles en tiempo de ejecución. Una vez creado, le pasaremos a nuestro
pool de hilos la clase que creamos anteriormente cuya función es dividir y
ejecutar nodos paralelamente.
Figura: #8

Esta pieza de código será la encargada de crear un objeto mediante el cuál


podremos usar para invocar nuestra clase SumatoriaBateriaForkJoin en un
pool o conjunto de hilos disponibles en nuestra máquina.

El método invoke lo que hará es ejecutar la tarea que definimos en nuestra


clase que implementa la libreria del framework Fork/Join
(SumatoriaBateriaForkJoin). Esta tarea, es entonces, lo que definimos en el
método compute(). Una vez invocamos nuestra clase, empieza la magia como
lo veremos a continuación.

Explicación animada de cómo funcionaría el método compute() para dividir y


sumar los nodos o sublistas creadas en paralelo hasta llegar al resultado final.
Conclusiones

Sin dudas que el patrón Fork/Join nos ayuda a lograr mejores tiempos de
respuesta, aunque su implementación hace más extensa la labor del
programador.

Por otro lado, teniendo la librería Stream de Java 8, que internamente


utiliza el patrón Fork/Join, encontramos tiempos de respuesta más altos; esto
anterior puede ser debido a que la librería administra la cantidad de hilos a
utilizar automáticamente en un pool de hilos para el framework llamada
ForkJoinPool.commonPool(), así que es algo por lo que no deberíamos
preocuparnos. Además, en pocas líneas podemos lograr lo mismo
utilizando la Stream API que hicimos creando la clase
SumatoriaBateriaForkJoin. Es importante decir también que haciendo la
implementación por nuestra cuenta del framework Fork/Join, podremos tener
dominio sobre la cantidad de subtareas y nuestro pool de hilos.

Bibliografía:
Medium.com/@breuner

https://github.com/AtolonRot/forkjoin-java

https://docs.oracle.com/javase/tutorial/essential/concurrency/forkjoin.ht
ml

https://es.wikipedia.org/wiki/Cola_doblemente_terminada

También podría gustarte