Concurrencia en Java: El Framework Ejecutor

Introducción

Con el aumento en el número de núcleos disponibles en los procesadores en la actualidad, junto con la necesidad cada vez mayor de lograr un mayor rendimiento, las API de subprocesamiento múltiple se están volviendo muy populares. Java proporciona su propio marco de subprocesos múltiples llamado el Marco Ejecutor .

¿Qué es el Marco Ejecutor?

El Marco Executor contiene una serie de componentes que se utilizan para administrar de manera eficiente los subprocesos de trabajo. La API del ejecutor anula la ejecución de la tarea de la tarea real que se ejecutará mediante ExecutorsEste diseño es una de las implementaciones del patrón Productor-Consumidor .
El java.util.concurrent.Executorsproveen métodos de fábrica que se pueden utilizar para crear ThreadPoolsde subprocesos de trabajo.
Para usar el Marco Executor necesitamos crear uno de esos grupos de hilos y enviar la tarea para su ejecución. Es el trabajo de Executor Framework programar y ejecutar las tareas enviadas y devolver los resultados del grupo de subprocesos.
Una pregunta básica que viene a la mente es ¿por qué necesitamos tales grupos de hilos cuando podemos crear objetos java.lang.Threado implementar RunnableCallableinterfaces para lograr el paralelismo?
La respuesta se reduce a dos hechos básicos:
  1. La creación de un nuevo subproceso para una nueva tarea conduce a una sobrecarga de creación y desmontaje de subprocesos. La gestión de este ciclo de vida del hilo aumenta significativamente el tiempo de ejecución.
  2. La adición de un nuevo hilo para cada proceso sin ninguna limitación conduce a la creación de un gran número de hilos. Estos hilos ocupan la memoria y causan un desperdicio de recursos. La CPU comienza a pasar demasiado tiempo cambiando los contextos cuando se intercambia cada hilo y entra otro hilo para su ejecución.
Todos estos factores reducen el rendimiento del sistema. Los grupos de subprocesos superan este problema manteniendo los subprocesos vivos y reutilizando los subprocesos. Cualquier exceso de tareas que fluyan en lo que los hilos en la agrupación pueden manejar se mantienen en a QueueUna vez que cualquiera de los hilos se libera, recogen la siguiente tarea de esta cola. Esta cola de tareas es esencialmente ilimitada para los ejecutores listos para usar que proporciona el JDK.

Tipos de Ejecutores

Ahora que tenemos una buena idea de lo que es un ejecutor, también echemos un vistazo a los diferentes tipos de ejecutores.

SingleThreadExecutor

Este ejecutor de grupo de subprocesos tiene un solo subproceso. Se utiliza para ejecutar tareas de forma secuencial. Si el subproceso muere debido a una excepción al ejecutar una tarea, se crea un nuevo subproceso para reemplazar el subproceso antiguo y las tareas subsiguientes se ejecutan en el nuevo.
ExecutorService executorService = Executors.newSingleThreadExecutor()  

FixedThreadPool (n)

Como su nombre lo indica, es un grupo de subprocesos de un número fijo de subprocesos. Las tareas enviadas al ejecutor son ejecutadas por los nsubprocesos y, si hay más tareas, se almacenan en un archivo LinkedBlockingQueueEste número suele ser el número total de subprocesos admitidos por el procesador subyacente.
ExecutorService executorService = Executors.newFixedThreadPool(4);  

CachedThreadPool

Este grupo de subprocesos se utiliza principalmente cuando hay muchas tareas paralelas de corta duración que deben ejecutarse. A diferencia del grupo de subprocesos fijos, el número de subprocesos de este grupo de ejecutores no está limitado. Si todos los subprocesos están ocupados ejecutando algunas tareas y llega una nueva tarea, el grupo creará y agregará un nuevo subproceso al ejecutor. Tan pronto como uno de los hilos se libere, asumirá la ejecución de las nuevas tareas. Si un hilo permanece inactivo durante sesenta segundos, se terminan y se eliminan de la caché.
Sin embargo, si no se administra correctamente o las tareas no son de corta duración, el grupo de subprocesos tendrá muchos subprocesos activos. Esto puede llevar a la agitación de recursos y, por tanto, a la caída del rendimiento.
ExecutorService executorService = Executors.newCachedThreadPool();  

Ejecutor Programado

Este ejecutor se utiliza cuando tenemos una tarea que debe ejecutarse a intervalos regulares o si queremos retrasar una tarea determinada.
ScheduledExecutorService scheduledExecService = Executors.newScheduledThreadPool(1);  
Las tareas se pueden programar ScheduledExecutorutilizando uno de los dos métodos scheduleAtFixedRatescheduleWithFixedDelay.
scheduledExecService.scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)  
scheduledExecService.scheduleWithFixedDelay(Runnable command, long initialDelay, long period, TimeUnit unit)  
La principal diferencia entre los dos métodos es su interpretación de la demora entre ejecuciones consecutivas de un trabajo programado.
scheduleAtFixedRate ejecuta la tarea con un intervalo fijo, independientemente de cuándo finalizó la tarea anterior.
scheduleWithFixedDelay iniciará la cuenta atrás de retraso solo después de que se complete la tarea actual.

Entendiendo el objeto futuro

Se puede acceder al resultado de la tarea enviada para su ejecución a un ejecutor utilizando el java.util.concurrent.Futureobjeto devuelto por el ejecutor. Se puede pensar en el futuro como una promesa hecha a la persona que llama por el ejecutor.
Future<String> result = executorService.submit(callableTask);  
Una tarea enviada al ejecutor, como la anterior, es asíncrona, es decir, la ejecución del programa no espera a que se complete la ejecución de la tarea para continuar con el siguiente paso. En cambio, cada vez que se completa la ejecución de la tarea, Futureel ejecutor la configura en este objeto.
La persona que llama puede continuar ejecutando el programa principal y cuando se necesita el resultado de la tarea enviada, puede llamar .get()a este Futureobjeto. Si la tarea se completa, el resultado se devuelve inmediatamente a la persona que llama o, de lo contrario, se bloquea hasta que el ejecutor finalice la ejecución y se calcule el resultado.
Si la persona que llama no puede permitirse esperar indefinidamente antes de recuperar el resultado, esta espera también puede cronometrarse. Esto se logra mediante el Future.get(long timeout, TimeUnit unit)método que arroja a TimeoutExceptionsi el resultado no se devuelve en el plazo estipulado. La persona que llama puede manejar esta excepción y continuar con la ejecución adicional del programa.
Si hay una excepción al ejecutar la tarea, el método de llamada para obtener lanzará un ExecutionException.
Una cosa importante con respecto al resultado que se devuelve por Future.get()método es que se devuelve solo si la tarea enviada se implementa java.util.concurrent.CallableSi la tarea implementa la Runnableinterfaz, la llamada .get()se devolverá nulluna vez que se complete la tarea.
Otro método importante es el Future.cancel(boolean mayInterruptIfRunning)método. Este método se utiliza para cancelar la ejecución de una tarea enviada. Si la tarea ya se está ejecutando, el ejecutor intentará interrumpir la ejecución de la tarea si el mayInterruptIfRunningindicador se pasa como true.

Ejemplo: crear y ejecutar un ejecutor simple

Ahora crearemos una tarea e intentaremos ejecutarla en un ejecutor de grupo fijo:
public class Task implements Callable<String> {

    private String message;

    public Task(String message) {
        this.message = message;
    }

    @Override
    public String call() throws Exception {
        return "Hello " + message + "!";
    }
}
La Taskclase implementa Callabley está parametrizada para Stringescribir. También se declara tirar ExceptionEsta capacidad de lanzar una excepción al ejecutor y al ejecutor que devuelve esta excepción a la persona que llama es de gran importancia porque ayuda a la persona que llama a conocer el estado de la ejecución de la tarea.
Ahora vamos a ejecutar esta tarea:
public class ExecutorExample {  
    public static void main(String[] args) {

        Task task = new Task("World");

        ExecutorService executorService = Executors.newFixedThreadPool(4);
        Future<String> result = executorService.submit(task);

        try {
            System.out.println(result.get());
        } catch (InterruptedException | ExecutionException e) {
            System.out.println("Error occured while executing the submitted task");
            e.printStackTrace();
        }

        executorService.shutdown();
    }
}
Aquí hemos creado un FixedThreadPoolejecutor con un conteo de 4 hilos desde que esta demostración se desarrolla en un procesador de cuatro núcleos. El número de subprocesos puede ser mayor que los núcleos del procesador si las tareas que se ejecutan realizan operaciones de E / S considerables o pasan tiempo esperando recursos externos.
Hemos creado una instancia de la Taskclase y la estamos pasando al ejecutor para su ejecución. El resultado es devuelto por el Futureobjeto, que luego imprimimos en la pantalla.
Vamos a ejecutar el ExecutorExampley comprobar su salida:
Hello World!  
Como se esperaba, la tarea agrega el saludo "Hola" y devuelve el resultado a través del Futureobjeto.
Por último, llamamos al cierre del executorServiceobjeto para terminar todos los subprocesos y devolver los recursos al sistema operativo.
El .shutdown()método espera la finalización de las tareas enviadas actualmente al ejecutor. Sin embargo, si el requisito es cerrar inmediatamente el ejecutor sin esperar, entonces podemos usar el .shutdownNow()método en su lugar.
Cualquier tarea pendiente de ejecución se devolverá en un java.util.Listobjeto.
También podemos crear esta misma tarea implementando la Runnableinterfaz:
public class Task implements Runnable{

    private String message;

    public Task(String message) {
        this.message = message;
    }

    public void run() {
        System.out.println("Hello " + message + "!");
    }
}
Hay algunos cambios importantes aquí cuando implementamos ejecutables.
  1. El resultado de la ejecución de la tarea no se puede devolver desde el run()método. Por lo tanto, estamos imprimiendo directamente desde aquí.
  2. El run()método no está configurado para lanzar excepciones comprobadas.

Conclusión

El subprocesamiento múltiple se está volviendo cada vez más común ya que la velocidad de reloj del procesador es difícil de aumentar. Sin embargo, manejar el ciclo de vida de cada hilo es muy difícil debido a la complejidad involucrada.
En este artículo, demostramos un marco de subprocesos múltiples eficiente y simple, el Marco de Ejecutor, y explicamos sus diferentes componentes. También observamos diferentes ejemplos de creación de tareas de envío y ejecución en un ejecutor.
Como siempre, el código para este ejemplo se puede encontrar en GitHub .

Acerca de: Programator

Somos Instinto Programador

0 comentarios:

Publicar un comentario

Dejanos tu comentario para seguir mejorando!

Con tecnología de Blogger.