Concurrencia en Java: el marco de ejecución

C

Introducción

Con el aumento en la cantidad de núcleos disponibles en los procesadores hoy en día, junto con la necesidad cada vez mayor de lograr un mayor rendimiento, las API de subprocesos múltiples se están volviendo bastante populares. Java proporciona su propio marco de subprocesos múltiples llamado Marco del ejecutor.

¿Qué es Executor Framework?

Executor Framework contiene una serie de componentes que se utilizan para administrar de manera eficiente los subprocesos de trabajo. La API del ejecutor desacopla la ejecución de la tarea de la tarea real que se ejecutará mediante Executors. Este diseño es una de las implementaciones del Productor-Consumidor patrón.

los java.util.concurrent.Executors proporcionar métodos de fábrica que se utilizan para crear ThreadPools de hilos de trabajo.

Para usar Executor Framework, necesitamos crear uno de esos grupos de subprocesos y enviarle la tarea para su ejecución. Es el trabajo del Executor Framework programar y ejecutar las tareas enviadas y devolver los resultados del grupo de subprocesos.

Una pregunta básica que me viene a la mente es por qué necesitamos tales grupos de subprocesos cuando podemos crear objetos de java.lang.Thread o implementar Runnable/Callable interfaces para lograr el paralelismo?

La respuesta se reduce a dos hechos básicos:

  • La creación de un nuevo hilo para una nueva tarea conlleva una sobrecarga de creación y desmontaje del hilo. La gestión del ciclo de vida de este hilo aumenta significativamente el tiempo de ejecución.
  • Agregar un nuevo hilo para cada proceso sin ningún tipo de limitación conduce a la creación de un gran número de hilos. Estos subprocesos ocupan memoria y provocan el desperdicio de recursos. La CPU comienza a dedicar demasiado tiempo a cambiar de contexto 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 vivos los subprocesos y reutilizándolos. Cualquier exceso de tareas que fluyan de lo que los subprocesos en el grupo pueden manejar se mantienen en un Queue. Una vez que alguno de los hilos se libera, retoman la siguiente tarea de esta cola. Esta cola de tareas es esencialmente ilimitada para los ejecutores listos para usar proporcionados por el JDK.

Tipos de ejecutores

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

SingleThreadExecutor

Este ejecutor de grupo de subprocesos tiene solo un subproceso. Se utiliza para ejecutar tareas de forma secuencial. Si el hilo muere debido a una excepción mientras se ejecuta una tarea, se crea un hilo nuevo para reemplazar el hilo antiguo y las tareas posteriores 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 el n subprocesos y si hay más tareas se almacenan en un LinkedBlockingQueue. Este 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 usa principalmente cuando hay muchas tareas paralelas de corta duración para ejecutar. 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, el grupo creará y agregará un nuevo subproceso al ejecutor. Tan pronto como uno de los subprocesos esté libre, retomará 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 dar lugar a una eliminación de recursos y, por tanto, a una caída del rendimiento.

ExecutorService executorService = Executors.newCachedThreadPool();

Fiscal programado

Este ejecutor se usa cuando tenemos una tarea que necesita ejecutarse a intervalos regulares o si deseamos retrasar una determinada tarea.

ScheduledExecutorService scheduledExecService = Executors.newScheduledThreadPool(1);

Las tareas se pueden programar en ScheduledExecutor usando cualquiera de los dos métodos scheduleAtFixedRate o scheduleWithFixedDelay.

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 del retraso entre ejecuciones consecutivas de un trabajo programado.

scheduleAtFixedRate ejecuta la tarea con un intervalo fijo, independientemente de cuándo terminó la tarea anterior.

scheduleWithFixedDelay iniciará la cuenta atrás del retraso solo después de que se complete la tarea actual.

Comprender el objeto futuro

Se puede acceder al resultado de la tarea enviada para su ejecución a un ejecutor utilizando el java.util.concurrent.Future objeto devuelto por el ejecutor. El futuro se puede considerar como una promesa hecha por el ejecutor a la persona que llama.

Future<String> result = executorService.submit(callableTask);

Una tarea enviada al ejecutor, como la anterior, es asincrónica, es decir, la ejecución del programa no espera a que se complete la ejecución de la tarea para pasar al siguiente paso. En cambio, siempre que se completa la ejecución de la tarea, se establece en este Future objeto por parte del albacea.

La persona que llama puede continuar ejecutando el programa principal y cuando se necesita el resultado de la tarea enviada, puede llamar .get() en este Future objeto. Si la tarea se completa, el resultado se devuelve inmediatamente a la persona que llama o, de lo contrario, la persona que llama se bloquea hasta que el ejecutor complete 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 la Future.get(long timeout, TimeUnit unit) método que arroja un TimeoutException si el resultado no se devuelve en el plazo estipulado. La persona que llama puede manejar esta excepción y continuar con la ejecución posterior del programa.

Si hay una excepción al ejecutar la tarea, la llamada al método get arrojará un ExecutionException.

Algo importante con respecto al resultado devuelto por Future.get() método es que se devuelve solo si la tarea enviada implementa java.util.concurrent.Callable. Si la tarea implementa el Runnable interfaz, la llamada a .get() volverá null una vez que la tarea esté completa.

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 mayInterruptIfRunning la bandera 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 + "!";
    }
}

los Task implementos de clase Callable y está parametrizado para String tipo. También se declara tirar Exception. Esta capacidad de lanzar una excepción al ejecutor y el ejecutor devolviendo esta excepción a la persona que llama es de gran importancia porque ayuda a la persona que llama a conocer el estado de ejecución de la tarea.

Ahora ejecutemos 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 FixedThreadPool ejecutor con una cuenta de 4 subprocesos ya 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 instanciado el Task class y se lo pasan al ejecutor para su ejecución. El resultado es devuelto por el Future objeto, que luego imprimimos en la pantalla.

Ejecutemos el ExecutorExample y verifique su salida:

Hello World!

Como se esperaba, la tarea agrega el saludo “Hola” y devuelve el resultado a través del Future objeto.

Por último, llamamos al cierre del executorService objeto para terminar todos los hilos y devolver los recursos al sistema operativo.

los .shutdown() El 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, podemos usar el .shutdownNow() método en su lugar.

Cualquier tarea pendiente de ejecución se devolverá en un java.util.List objeto.

También podemos crear esta misma tarea implementando el Runnable interfaz:

public class Task implements Runnable{

    private String message;

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

    public void run() {
        System.out.println("Hello " + message + "!");
    }
}

Hay un par de cambios importantes aquí cuando implementamos runnable.

  • 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í.
  • los run() El método no está configurado para lanzar ninguna excepción marcada.

Conclusión

El subproceso múltiple se está volviendo cada vez más común ya que la velocidad del 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últiple eficiente pero simple, Executor Framework, y explicamos sus diferentes componentes. También echamos un vistazo a diferentes ejemplos de creación de envío y ejecución de tareas en un ejecutor.

Como siempre, el código de este ejemplo se puede encontrar en GitHub.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con tus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad