sql >> Base de Datos >  >> RDS >> Database

Conceptos básicos de programación paralela con Fork/Join Framework en Java

Con la llegada de las CPU multinúcleo en los últimos años, la programación paralela es la forma de aprovechar al máximo los nuevos caballos de fuerza de procesamiento. Programación paralela se refiere a la ejecución concurrente de procesos debido a la disponibilidad de múltiples núcleos de procesamiento. Esto, en esencia, conduce a un tremendo aumento en el rendimiento y la eficiencia de los programas en contraste con la ejecución lineal de un solo núcleo o incluso con subprocesos múltiples. El marco Fork/Join es parte de la API de concurrencia de Java. Este marco permite a los programadores paralelizar algoritmos. Este artículo explora el concepto de programación paralela con la ayuda de Fork/Join Framework disponible en Java.

Una visión general

La programación paralela tiene una connotación mucho más amplia y, sin duda, es un área muy amplia para desarrollar en unas pocas líneas. El quid de la cuestión es bastante simple, pero operativamente mucho más difícil de lograr. En términos simples, la programación paralela significa escribir programas que usan más de un procesador para completar una tarea, ¡eso es todo! Adivina qué; suena familiar, ¿no? Casi rima con la idea de subprocesos múltiples. Pero, tenga en cuenta que hay algunas distinciones importantes entre ellos. En la superficie, son lo mismo, pero el trasfondo es absolutamente diferente. De hecho, los subprocesos múltiples se introdujeron para proporcionar una especie de ilusión de procesamiento paralelo sin ninguna ejecución paralela real. Lo que realmente hace el subprocesamiento múltiple es que roba el tiempo de inactividad de la CPU y lo utiliza en su beneficio.

En resumen, los subprocesos múltiples son una colección de unidades lógicas discretas de tareas que se ejecutan para aprovechar su parte del tiempo de la CPU, mientras que otro subproceso puede estar esperando temporalmente, por ejemplo, alguna entrada del usuario. El tiempo de inactividad de la CPU se comparte de manera óptima entre los subprocesos de la competencia. Si solo hay una CPU, es tiempo compartido. Si hay varios núcleos de CPU, también se comparten en todo momento. Por lo tanto, un programa multiproceso óptimo exprime el rendimiento de la CPU mediante el ingenioso mecanismo de tiempo compartido. En esencia, siempre es un subproceso que usa una CPU mientras otro subproceso está esperando. Esto sucede de una manera sutil en la que el usuario tiene la sensación de un procesamiento paralelo cuando, en realidad, el procesamiento se lleva a cabo en rápida sucesión. La mayor ventaja de los subprocesos múltiples es que es una técnica para aprovechar al máximo los recursos de procesamiento. Ahora, esta idea es bastante útil y se puede usar en cualquier conjunto de entornos, ya sea que tenga una sola CPU o varias CPU. La idea es la misma.

La programación paralela, por otro lado, significa que hay múltiples CPU dedicadas que el programador aprovecha en paralelo. Este tipo de programación está optimizado para un entorno de CPU multinúcleo. La mayoría de las máquinas actuales utilizan CPU multinúcleo. Por lo tanto, la programación paralela es bastante relevante hoy en día. Incluso la máquina más económica está equipada con CPU multinúcleo. Mire los dispositivos de mano; incluso son multinúcleo. Aunque todo parece marchar sobre ruedas con las CPU multinúcleo, aquí también hay otro lado de la historia. ¿Más núcleos de CPU significan una computación más rápida o eficiente? ¡No siempre! La filosofía codiciosa de "cuanto más, mejor" no se aplica a la informática, ni en la vida. Pero están ahí, imperceptiblemente:dual, quad, octa, etc. Están ahí principalmente porque los queremos y no porque los necesitemos, al menos en la mayoría de los casos. En realidad, es relativamente difícil mantener ocupada incluso una sola CPU en la informática diaria. Sin embargo, los multinúcleos tienen su uso en circunstancias especiales, como en servidores, juegos, etc., o para resolver grandes problemas. El problema de tener varias CPU es que requiere una memoria que debe igualar la velocidad con la potencia de procesamiento, junto con canales de datos ultrarrápidos y otros accesorios. En resumen, varios núcleos de CPU en la informática diaria proporcionan una mejora del rendimiento que no puede compensar la cantidad de recursos necesarios para usarlos. En consecuencia, obtenemos una máquina costosa infrautilizada, tal vez solo para ser exhibida.

Programación paralela

A diferencia de los subprocesos múltiples, donde cada tarea es una unidad lógica discreta de una tarea más grande, las tareas de programación paralela son independientes y su orden de ejecución no importa. Las tareas se definen de acuerdo con la función que realizan o los datos utilizados en el procesamiento; esto se llama paralelismo funcional o paralelismo de datos , respectivamente. En el paralelismo funcional, cada procesador trabaja en su sección del problema, mientras que en el paralelismo de datos, el procesador trabaja en su sección de datos. La programación en paralelo es adecuada para una base de problemas más grande que no encaja en una sola arquitectura de CPU, o puede ser que el problema sea tan grande que no se pueda resolver en un tiempo estimado razonable. Como resultado, las tareas, cuando se distribuyen entre los procesadores, pueden obtener el resultado relativamente rápido.

El marco de bifurcación/unión

El marco Fork/Join se define en java.util.concurrent paquete. Incluye varias clases e interfaces que admiten la programación paralela. Lo que hace principalmente es que simplifica el proceso de creación de múltiples subprocesos, sus usos y automatiza el mecanismo de asignación de procesos entre múltiples procesadores. La notable diferencia entre multiproceso y programación paralela con este marco es muy similar a lo que mencionamos anteriormente. Aquí, la parte de procesamiento está optimizada para usar múltiples procesadores, a diferencia de los subprocesos múltiples, donde el tiempo de inactividad de la CPU única se optimiza en función del tiempo compartido. La ventaja añadida con este marco es usar subprocesos múltiples en un entorno de ejecución paralelo. No hay daño allí.

Hay cuatro clases principales en este marco:

  • ForkJoinTask: Esta es una clase abstracta que define una tarea. Por lo general, una tarea se crea con la ayuda de fork() método definido en esta clase. Esta tarea es casi similar a un hilo normal creado con el Hilo clase, pero es más ligera que ella. El mecanismo que aplica es que permite la gestión de una gran cantidad de tareas con la ayuda de una pequeña cantidad de subprocesos reales que se unen al ForkJoinPool . El tenedor() El método permite la ejecución asincrónica de la tarea de invocación. El unirse() El método permite esperar hasta que la tarea en la que se llama finalice finalmente. Hay otro método, llamado invoke() , que combina el tenedor y únete operaciones en una sola llamada.
  • ForkJoinPool: Esta clase proporciona un grupo común para administrar la ejecución de ForkJoinTask Tareas. Básicamente proporciona el punto de entrada para los envíos que no sean ForkJoinTask clientes, así como operaciones de gestión y seguimiento.
  • Acción recursiva: Esta es también una extensión abstracta de ForkJoinTask clase. Por lo general, extendemos esta clase para crear una tarea que no devuelve un resultado o tiene un vacío tipo de retorno. El computar() El método definido en esta clase se anula para incluir el código computacional de la tarea.
  • Tarea recursiva: Esta es otra extensión abstracta de ForkJoinTask clase. Extendemos esta clase para crear una tarea que devuelva un resultado. Y, al igual que ResursiveAction, también incluye un recurso abstracto protegido() método. Este método se anula para incluir la parte de cálculo de la tarea.

La estrategia del marco Fork/Join

Este marco emplea un recursivo divide y vencerás estrategia para implementar el procesamiento paralelo. Básicamente divide una tarea en subtareas más pequeñas; luego, cada subtarea se divide a su vez en sub-subtareas. Este proceso se aplica recursivamente en cada tarea hasta que sea lo suficientemente pequeño para ser manejado secuencialmente. Supongamos que vamos a incrementar los valores de una matriz de N números. Esta es la tarea. Ahora, podemos dividir la matriz por dos creando dos subtareas. Vuelva a dividir cada una de ellas en dos subtareas más, y así sucesivamente. De esta forma, podemos aplicar un divide y vencerás estrategia recursivamente hasta que las tareas se individualizan en un problema unitario. Este problema de unidad puede ser ejecutado en paralelo por los múltiples procesadores centrales disponibles. En un entorno no paralelo, lo que teníamos que hacer era recorrer todo el conjunto y realizar el procesamiento en secuencia. Este es claramente un enfoque ineficiente en vista del procesamiento paralelo. Pero, la verdadera pregunta es si todos los problemas pueden ser divididos y conquistados ? ¡Definitivamente no! Pero hay problemas que a menudo involucran algún tipo de matriz, colección o agrupación de datos que se adapta particularmente a este enfoque. Por cierto, hay problemas que pueden no usar la recopilación de datos pero pueden optimizarse para usar la estrategia de programación paralela. Qué tipo de problemas computacionales son adecuados para el procesamiento paralelo o la discusión sobre algoritmos paralelos está fuera del alcance de este artículo. Veamos un ejemplo rápido sobre la aplicación de Fork/Join Framework.

Un ejemplo rápido

Este es un ejemplo muy simple para darle una idea de cómo implementar el paralelismo en Java con Fork/Join Framework.

package org.mano.example;
import java.util.concurrent.RecursiveAction;
public class CustomRecursiveAction extends
      RecursiveAction {
   final int THRESHOLD = 2;
   double [] numbers;
   int indexStart, indexLast;
   CustomRecursiveAction(double [] n, int s, int l) {
      numbers = n;
      indexStart = s;
      indexLast = l;
   }
   @Override
   protected void compute() {
      if ((indexLast - indexStart) > THRESHOLD)
         for (int i = indexStart; i < indexLast; i++)
            numbers [i] = numbers [i] + Math.random();
         else
            invokeAll (new CustomRecursiveAction(numbers,
               indexStart, (indexStart - indexLast) / 2),
               new CustomRecursiveAction(numbers,
                  (indexStart - indexLast) / 2,
                     indexLast));
   }
}

package org.mano.example;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
public class Main {
   public static void main(String[] args) {
      final int SIZE = 10;
      ForkJoinPool pool = new ForkJoinPool();
      double na[] = new double [SIZE];
      System.out.println("initialized random values :");
      for (int i = 0; i < na.length; i++) {
         na[i] = (double) i + Math.random();
         System.out.format("%.4f ", na[i]);
      }
      System.out.println();
      CustomRecursiveAction task = new
         CustomRecursiveAction(na, 0, na.length);
      pool.invoke(task);
      System.out.println("Changed values :");
      for (inti = 0; i < 10; i++)
      System.out.format("%.4f ", na[i]);
      System.out.println();
   }
}

Conclusión

Esta es una breve descripción de la programación paralela y cómo se admite en Java. Es un hecho bien establecido que tener N cores no va a hacer todo N veces más rápido Solo una parte de las aplicaciones Java utilizan esta función de forma eficaz. El código de programación paralelo es un marco difícil. Además, los programas paralelos eficaces deben tener en cuenta cuestiones como el equilibrio de carga, la comunicación entre tareas paralelas y similares. Hay algunos algoritmos que se adaptan mejor a la ejecución en paralelo, pero muchos no. En cualquier caso, a la API de Java no le falta soporte. Siempre podemos jugar con las API para descubrir qué se adapta mejor. Codificación feliz 🙂