Executor Framework para trabajo con tareas Java

java IconoLas hilos proporcionan capacidad multitarea a los procesos, entendiendo proceso como un programa en ejecución. Un programa puede involucrar múltiples hilos; cada uno de ellos proporciona una unidad de control. Un programa mono hilo se ejecuta de forma predecible de principio a fin. Por otro lado, un programa multihilo nos brinda la esencia de la concurrencia o ejecución simultánea de instrucciones donde un subconjunto de las mismas se supone que se ejecutan de forma paralela. Este mecanismo eleva el rendimiento, especialmente debido a que la mayor parte de los procesadores actuales son multi-core. Por lo tanto, y en definitiva, emplear un solo programa mono-hilo que emplea únicamente un core del procesador es una pérdida recursos.

Las APIs core de Java incluyen un framework de nombre Executors, que proporciona ayudas al programador cuando se encuentra trabajando en un entorno multihilo. Este articulo se centra en este framework y proporcionará unas breves pinceladas para que el lector pueda empezar con él.

Ejecución paralela

La ejecución paralela requiere de alguna ayuda adicional de hardware, un programa multihilo no es una excepción. Los programas multihilo pueden emplear los múltiples cores que a día de hoy existen en las máquinas modernas, obteniendo un rendimiento muy superior a los mono hilo. El problema de todo este asunto es que para emplear al máximo las capacidades de los núcleos existentes, el software debe escribirse teniendo en cuenta esta arquitectura desde el comienzo. En la práctica esto es mas fácil decirlo que hacerlo

Cierta lógica es clara candidata al paralelismo mientras que otra no lo es tanto, por lo tanto el problema principal es balancear para maximizar el uso de los recursos de forma adecuada y por otra parte no romper la estructura del programa. La lógica paralela es inherentemente paralela y dicha implementación es casi obvia, pero convertir un problema lineal en algo paralelo y óptimo no lo es tanto. Para que nos hagamos una idea, la solución de 2 + 2 = 4 es bastante lineal y encaja perfectamente en dicha premisa lineal, pero la lógica para resolver una expresión como (2 x 4) + (5 / 2) de forma paralela en la que podría encajar perfectamente no es tan obvia.

Antigua implementación

Existen muchos aspectos a considerar antes de modelar un programa con una aproximación multihilo. Podemos considerar algunas preguntas iniciales al respecto:

  • ¿Cómo se crea y se envía la tarea a ejecutar?
  • ¿Existen dependencias relacionadas con la tarea para su ejecución?
  • ¿Se presenta ejecución sincrona o asincrona?
  • ¿Como vamos a tratar un error en ejecución en la tarea?
  • ¿Quién ejecutará la tarea?
  • ¿Cómo obtendremos el retorno de la tarea una vez se ha completado?

Cuando creamos una tarea, tarea como unidad individual de trabajo, lo que normalmente hacemos es o bien implementar el interfaz llamado Runnable o bien extender de la clase Thread class:


public class SampleTask implementsRunnable {
   //...
   public void run(){
      //...
   }
}

Ahora creamos la tarea de la siguiente forma:

SampleTask st1=new SampleTask();
SampleTask st2=new SampleTask();

Y ejecutamos:

Thread t1=new Thread(st1);
Thread t2=new Thread(st2);

t1.start();
t2.start();

Para obtener información sobre una de las tareas debemos escribir un poco mas de código. El asunto es que tal y como podemos ver, la gestión de las tareas requiere de un control muy fino por nuestra parte, tales como la creacion y destruccion de las tareas creadas, además esto afecta al tiempo requerido para crear nuevas tareas. Si todo esto no se ejecuta de forma organizada, obviamente los tiempos de creación de nuevas tareas van a verse afectados. Una tarea consume recursos, y obviamente múltiples tareas consumen dichos recursos multiplicados por el número de tareas. Esta aproximación tiende a machacar el rendimiento de la CPU; peor aún, puede incluso tirar el sistema si el número de tareas existente sobrepasa el límite admitido por el sistema operativo. También puede suceder que una tarea consuma recursos destinados a las demás dejando las otras bloqueadas.

Como el lector puede apreciar, la complejidad para gestionar un programa que presenta este tipo de arquitectura es alta.

Executor framework

Executor Framework trata de dirigir todos estos problemas y proporcionar algo más de control. El primer aspecto importante que podemos encontrar en este framework es la distinción entre la ejecución de una tarea y el envío de la misma. El ejecutor específica, crea la tarea y envíamela, ya me encargo yo del resto. Por lo tanto en lugar de crear una tarea de forma explícita, el código antiguo puede reescribirse del siguiente modo:

Executor executor=Executors.newFixedThreadPool(5);

Y entonces:

executor.execute(new SampleTask());   // o executor.execute(st1);
executor.execute(new SampleTask());   // o executor.execute(st2);

La llamada al método execute de executor no asegura que la ejecución de la tarea haya sido iniciada; en su lugar, simplemente se refiere el envío de la misma. El executor toma la responsabilidad en su nombre, incluyendo los detalles de las políticas durante el transcurso de la ejecución de la misma. La librería de clases proporcionada por el framework executor determina la política, que por supuesto es configurable.

Existen multitud de métodos estáticos disponibles en la clase Executors (Executor es un interfaz y Executors es una clase. Ambos incluidos en el paquete java.util.concurrent). Unos pocos de los mas usados podrían ser:

  • newCachedThreadPool(): Crea un nuevo pool de tarea cuando es necesario, pero intenta reusar cualquier tarea previamente construida. El pool de tareas puede reducirse y expandirse, dependiendo de la carga.
  • newFixedThreadPool(int nThreads): Pool de tamaño fijo.
  • newSingleThreadExecutor(): Este método asegura que se ejecutará un solo hilo que será el que ejecute las tareas. Si este hilo finaliza inesperadamente se crea uno nuevo.Está garantizado que siempre va a existir un único hilo dado un momento cualquiera.

Todos estos métodos retornan un objeto ExecutorService.

El interface ExecutorService extiende Executor y proporciona los métodos necesarios para gestionar la ejecución de las tareas, tales como la finalización ordenada de las mismas. Existe otra interfaz llamada ScheduledExecutorService, que extiende ExecutorService y que soporta la programación de tareas.

Ejemplo

Veamos un programa rápido para entender el ejecutor:

package org.example;

public class MyTask implements Runnable {

   private int id;
   private int counter;

   public MyTask(int id, int counter) {
      this.id = id;
      this.counter = counter;
   }

   @Override
   public void run() {
      for (int i = 0; i < counter; i++) {
         try {
            System.out.println("Task ID: " + id + " Iter No: " + i);
            Thread.sleep(1000);
         } catch (Exception ex) {
            System.out.println("Task ID: " + id + " is interrupted.");
            break;
         }
      }
   }
}

package org.example;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestMyTask {

   public static void main(String[] args) {
      ExecutorService executorService =
         Executors.newFixedThreadPool(2);
      for (int i = 0; i < 3; i++)
         executorService.submit(new MyTask(i, 5));
      executorService.shutdown();
   }
}

Conclusión

El FrameworkExecutor es una de las muchas ayudas proporcionadas por las APIS core de Java, especialmente pensada para trabajar con ejecución concurrente o en la creación de aplicaciones multitarea. Otras librerías útiles para programación concurrente podrían ser las siguientes explicit locks, synchronizer, atomic variables, y fork/join framework, y obviamente el framework executor. La separación entre el envío y la ejecución es la gran ventaja del framework que acabamos de ver. Los que os dedicais a desarrollo seguro que le encontráis mil utilidades a este framework para lograr reducir la complejidad de los desarrollos relativos a multihilo.

Referencias

https://docs.oracle.com/javase/tutorial/essential/concurrency/index.html
https://docs.oracle.com/javase/8/docs/api/index.html