Concurrencia en Java: el marco de ejecuci贸n

    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.

     

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *