Gu铆a para la interfaz del futuro en Java

    Introducci贸n

    En este art铆culo, repasaremos la funcionalidad de la interfaz Future como una de las construcciones de concurrencia de Java. Tambi茅n veremos varias formas de crear una tarea asincr贸nica, porque un Future es solo una forma de representar el resultado de un c谩lculo asincr贸nico.

    los java.util.concurrent El paquete se agreg贸 a Java 5. Este paquete contiene un conjunto de clases que facilita el desarrollo de aplicaciones concurrentes en Java. En general, la concurrencia es un tema bastante complejo y puede parecer un poco abrumador.

    Una java Future es muy similar a JavaScript Promise.

    Motivaci贸n

    Una tarea com煤n para el c贸digo asincr贸nico es proporcionar una interfaz de usuario receptiva en una aplicaci贸n que ejecuta un c谩lculo costoso o una operaci贸n de lectura / escritura de datos.

    Tener una pantalla congelada o ninguna indicaci贸n de que el proceso est谩 en progreso resulta en una experiencia de usuario bastante mala. Lo mismo ocurre con las aplicaciones que son completamente lentas:

    La minimizaci贸n del tiempo de inactividad mediante el cambio de tareas puede mejorar dr谩sticamente el rendimiento de una aplicaci贸n, aunque depende del tipo de operaciones involucradas.

    La obtenci贸n de un recurso web puede demorarse o ser lenta en general. La lectura de un archivo enorme puede resultar lenta. Esperar un resultado de microservicios en cascada puede ser lento. En arquitecturas s铆ncronas, la aplicaci贸n que espera el resultado espera a que se completen todos estos procesos antes de continuar.

    En arquitecturas asincr贸nicas, contin煤a haciendo las cosas que puede sin el resultado devuelto mientras tanto.

    Implementaci贸n

    Antes de comenzar con ejemplos, veamos las interfaces y clases b谩sicas del java.util.concurrent paquete que vamos a utilizar.

    El java Callable interfaz es una versi贸n mejorada de Runnable. Representa una tarea que devuelve un resultado y puede generar una excepci贸n. Para implementar Callable, tienes que implementar el call() m茅todo sin argumentos.

    Para enviar nuestro Callable para la ejecuci贸n concurrente, usaremos el ExecutorService. La forma m谩s sencilla de crear un ExecutorService es utilizar uno de los m茅todos de f谩brica del Executors clase. Despu茅s de la creaci贸n de la tarea asincr贸nica, Java Future el objeto es devuelto por el ejecutor.

    Si desea leer m谩s sobre The Executor Framework, tenemos un art铆culo detallado sobre eso.

    La interfaz del futuro

    los Future interfaz es una interfaz que representa un resultado que eventualmente se devolver谩 en el futuro. Podemos comprobar si un Future ha recibido el resultado, si est谩 esperando un resultado o si ha fallado antes de intentar acceder a 茅l, lo cual cubriremos en las pr贸ximas secciones.

    Primero echemos un vistazo a la definici贸n de interfaz:

    public interface Future<V> {
    	V get() throws InterruptedException, ExecutionException;
    	V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
    	boolean isCancelled();
    	boolean isDone();
    	boolean cancel(boolean mayInterruptIfRunning)
    }
    

    los get() m茅todo recupera el resultado. Si el resultado a煤n no se ha devuelto a un Future ejemplo, el get() esperar谩 a que se devuelva el resultado. Es crucial tener en cuenta que get() bloquear谩 su aplicaci贸n si la llama antes de que se haya devuelto el resultado.

    Tambi茅n puede especificar un timeout despu茅s de lo cual el get() El m茅todo lanzar谩 una excepci贸n si el resultado a煤n no se ha devuelto, lo que evitar谩 grandes cuellos de botella.

    los cancel() El m茅todo intenta cancelar la ejecuci贸n de la tarea actual. El intento fallar谩 si la tarea ya se complet贸, se cancel贸 o no se pudo cancelar debido a otras razones.

    los isDone() y isCancelled() Los m茅todos est谩n dedicados a averiguar el estado actual de un Callable tarea. Por lo general, los usar谩 como condicionales para verificar si tiene sentido usar el get() o cancel() m茅todos.

    La interfaz invocable

    Creemos una tarea que tarde un poco en completarse. Definiremos un DataReader ese implements Callable:

    public class DataReader implements Callable {
        @Override
        public String call() throws Exception {
            System.out.println("Reading data...");
            TimeUnit.SECONDS.sleep(5);
            return "Data reading finished";
        }
    }
    

    Para simular una operaci贸n costosa, usamos TimeUnit.SECONDS.sleep(). Llama Thread.sleep(), pero es un poco m谩s limpio durante per铆odos de tiempo m谩s largos.

    De manera similar, tengamos una clase de procesador que procese algunos otros datos al mismo tiempo:

    public class DataProcessor implements Callable {
        @Override
        public String call() throws Exception {
            System.out.println("Processing data...");
            TimeUnit.SECONDS.sleep(5);
            return "Data is processed";
        }
    }
    

    Ambos m茅todos tardan 5 segundos en ejecutarse. Si tuvi茅ramos que llamar uno tras otro sincr贸nicamente, la lectura y el procesamiento tomar铆an ~ 10 segundos.

    Ejecuci贸n de tareas futuras

    Ahora, para llamar a estos m茅todos desde otro, crearemos una instancia de un ejecutor y enviaremos nuestro DataReader y DataProcessor lo. El ejecutor devuelve un Future, as铆 que empaquetaremos el resultado en un Future-objeto envuelto:

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
    
        Future<String> dataReadFuture = executorService.submit(new DataReader());
        Future<String> dataProcessFuture = executorService.submit(new DataProcessor());
    
        while (!dataReadFuture.isDone() && !dataProcessFuture.isDone()) {
                System.out.println("Reading and processing not yet finished.");
                // Do some other things that don't depend on these two processes
                // Simulating another task
                TimeUnit.SECONDS.sleep(1);
            }
        System.out.println(dataReadFuture.get());
        System.out.println(dataProcessFuture.get());
    }
    

    Aqu铆, hemos creado un ejecutor con dos hilos en el grupo ya que tenemos dos tareas. Puedes usar el newSingularThreadExecutor() para crear una sola si solo tiene una tarea simult谩nea para ejecutar.

    Si enviamos m谩s de estas dos tareas a este grupo, las tareas adicionales esperar谩n en la cola hasta que aparezca un lugar libre.

    Ejecutar este fragmento de c贸digo producir谩:

    Reading and processing not yet finished.
    Reading data...
    Processing data...
    Reading and processing not yet finished.
    Reading and processing not yet finished.
    Reading and processing not yet finished.
    Reading and processing not yet finished.
    Data reading finished
    Data is processed
    

    El tiempo de ejecuci贸n total ser谩 ~ 5 segundos, no ~ 10 segundos, ya que ambos se ejecutaban simult谩neamente al mismo tiempo. Tan pronto como hayamos enviado las clases al albacea, su call() Se han llamado m茅todos. Incluso teniendo un Thread.sleep() de un segundo cinco veces no afecta mucho al rendimiento, ya que se ejecuta en su propio hilo.

    Es importante tener en cuenta que el c贸digo no se ejecut贸 m谩s r谩pido, simplemente no esper贸 de manera redundante por algo que no ten铆a que hacer y realiz贸 otras tareas mientras tanto.

    Lo importante aqu铆 es el uso de isDone() m茅todo. Si no tuvi茅ramos el cheque, no habr铆a ninguna garant铆a de que los resultados estuvieran empaquetados en el Futures antes de que hayamos accedido a ellos. Si no lo fueran, el get() Los m茅todos bloquear铆an la aplicaci贸n hasta que tuvieran resultados.

    Tiempo de espera futuro

    Si no hubo comprobaciones para la finalizaci贸n de tareas futuras:

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
    
        Future<String> dataReadFuture = executorService.submit(new DataReader());
        Future<String> dataProcessFuture = executorService.submit(new DataProcessor());
    
        System.out.println("Doing another task in anticipation of the results.");
        // Simulating another task
        TimeUnit.SECONDS.sleep(1);
        System.out.println(dataReadFuture.get());
        System.out.println(dataProcessFuture.get());
    }
    

    El tiempo de ejecuci贸n a煤n ser铆a de ~ 5 segundos, sin embargo, nos enfrentar铆amos a un gran problema. Se tarda 1 segundo en completar una tarea adicional y 5 en completar las otras dos.

    驴Suena como la 煤ltima vez?

    4 de cada 5 segundos en este programa est谩n bloqueando. Intentamos obtener el resultado del futuro antes de que se devolviera y bloqueamos 4 segundos hasta que regresen.

    Establezcamos una restricci贸n para obtener estos m茅todos. Si no regresan dentro de un cierto per铆odo de tiempo esperado, lanzar谩n excepciones:

    String dataReadResult = null;
    String dataProcessResult = null;
    
    try {
        dataReadResult = dataReadFuture.get(4, TimeUnit.SECONDS);
        dataProcessResult = dataProcessFuture.get(0, TimeUnit.SECONDS);
    } catch (InterruptedException | ExecutionException | TimeoutException e) {
        e.printStackTrace();
    }
    
    System.out.println(dataReadResult);
    System.out.println(dataProcessResult);
    

    Ambos toman 5 chelines cada uno. Con una espera inicial de un segundo de la otra tarea, el dataReadFuture se devuelve en 4 segundos adicionales. El resultado del proceso de datos se devuelve al mismo tiempo y este c贸digo se ejecuta bien.

    Si le d谩ramos un tiempo poco realista para ejecutarse (menos de 5 segundos en total), ser铆amos recibidos con:

    Reading data...
    Doing another task in anticipation of the results.
    Processing data...
    java.util.concurrent.TimeoutException
    	at java.util.concurrent.FutureTask.get(FutureTask.java:205)
    	at FutureTutorial.Main.main(Main.java:21)
    	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    	at java.lang.reflect.Method.invoke(Method.java:497)
    	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
    null
    null
    

    Por supuesto, no imprimir铆amos simplemente el seguimiento de la pila en una aplicaci贸n real, sino que redirigir铆amos la l贸gica para manejar el estado excepcional.

    Cancelaci贸n de futuros

    En algunos casos, es posible que desee cancelar un futuro. Por ejemplo, si no recibe un resultado dentro de n segundos, es posible que decida no utilizar el resultado en absoluto. En ese caso, no es necesario que un hilo a煤n se ejecute y empaque el resultado, ya que no lo usar谩.

    De esta manera, libera un espacio para otra tarea en la cola o simplemente libera los recursos asignados a una operaci贸n costosa innecesaria:

    boolean cancelled = false;
    if (dataReadFuture.isDone()) {
        try {
            dataReadResult = dataReadFuture.get();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    } else {
    cancelled = dataReadFuture.cancel(true);
    }
    if (!cancelled) {
        System.out.println(dataReadResult);
    } else {
        System.out.println("Task was cancelled.");
    }
    

    Si la tarea se ha realizado, obtenemos el resultado y lo empaquetamos en nuestra Cadena de resultados. De lo contrario, nosotros cancel() eso. Si no fuera cancelled, imprimimos el valor de la cadena resultante. Por el contrario, notificamos al usuario que la tarea se cancel贸 de otro modo.

    Lo que vale la pena se帽alar es que cancel() el m茅todo acepta un boolean par谩metro. Esta boolean define si permitimos el cancel() m茅todo para interrumpir la ejecuci贸n de la tarea o no. Si lo configuramos como false, existe la posibilidad de que la tarea no se cancele.

    Tenemos que asignar el valor de retorno del cancel() m茅todo a un boolean tambi茅n. El valor devuelto significa si el m茅todo se ejecut贸 correctamente o no. Si no puede cancelar una tarea, el boolean se establecer谩 como false.

    Ejecutar este c贸digo producir谩:

    Reading data...
    Processing data...
    Task was cancelled.
    

    Y si intentamos obtener los datos de una tarea cancelada, CancellationException es generado:

    if (dataReadFuture.cancel(true)) {
        dataReadFuture.get();
    }
    

    Ejecutar este c贸digo producir谩:

    Processing data...
    Exception in thread "main" java.util.concurrent.CancellationException
    	at java.util.concurrent.FutureTask.report(FutureTask.java:121)
    	at java.util.concurrent.FutureTask.get(FutureTask.java:192)
    	at FutureTutorial.Main.main(Main.java:34)
    	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    	at java.lang.reflect.Method.invoke(Method.java:497)
    	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
    

    Limitaciones del futuro

    El java Future fue un buen paso hacia la programaci贸n asincr贸nica. Pero, como ya puede tener avisos, es rudimentario:

    • Futures no se puede completar expl铆citamente (estableciendo su valor y estado).
    • No tiene un mecanismo para crear etapas de procesamiento que est茅n encadenadas.
    • No hay mecanismo para ejecutar Futures en paralelo y despu茅s para combinar sus resultados.
    • los Future no tiene construcciones de manejo de excepciones.

    Afortunadamente, Java proporciona implementaciones futuras concretas que brindan estas caracter铆sticas (CompletableFuture, CountedCompleter, ForkJoinTask, FutureTask, etc.).

    Conclusi贸n

    Cuando necesite esperar a que se complete otro proceso sin bloquear, puede ser 煤til volverse asincr贸nico. Este enfoque ayuda a mejorar la usabilidad y el rendimiento de las aplicaciones.

    Java incluye construcciones espec铆ficas para la concurrencia. El b谩sico es el Java Future que representa el resultado del c谩lculo asincr贸nico y proporciona m茅todos b谩sicos para manejar el proceso.

    Etiquetas:

    Deja una respuesta

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