Programación

1º DAM/DAW - Curso 2023-2024

User Tools

Site Tools


apuntes:concurrencia

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revisionBoth sides next revision
apuntes:concurrencia [2021/03/10 13:18] – created Santiago Faciapuntes:concurrencia [2021/03/10 13:19] Santiago Faci
Line 298: Line 298:
  
  
-===== +===== Programación concurrente ===== 
 + 
 +\part*{Programación concurrente} 
 + 
 +\section{Executors y pools de hilos} 
 + 
 +En el apartado anterior vimos como crear y lanzar hilos individualmente utilizando la clase \verb Thread  que Java proporcina en su API. El 
 +código funciona perfectamente pero realmente se vuelve complicado de desarrollar si lo que tenemos que gestionar es una gran cantidad de 
 +hilos de forma simultánea. Para esos casos, Java proporciona un framework, \emph{Executors}, que permite una gestión mucho más sencillo de 
 +cualquier tarea que se ejecuta en segundo plano. 
 + 
 +Utilizando \emph{Executors}, como programador, te olvidas en parte de la gestión de los hilos. Simplemente hay que centrarse en programar 
 +el código a realizar y asignar dicho código (una clase \verb Runnable ) a un \verb Executor , que se encargará de crear el hilo, lanzarlo y 
 +gestionar su ejecución. 
 + 
 +Veamos un ejemplo de cómo lanzar una serie de tareas en segundo plano utilizando la clase \verb ExecutorService  que viene con el framework 
 +\emph{Executors} de Java. 
 + 
 +Supongamos una clase muy sencilla que implementa \verb Runnable  para definir un código que debe ejecutarse en segundo plano: 
 + 
 +\begin{lstlisting}[language=java] 
 +public class Tarea implements Runnable { 
 +    @Override 
 +    public void run() { 
 +        System.out.println("Soy una clase que implementa Runnable"); 
 +        System.out.println("Ya me termino"); 
 +    } 
 +
 +\end{lstlisting} 
 + 
 +Para lanzar la tarea, o varias instancia de la misma, podemos crear un pool de hilos (en este caso dos) y pasarles las tareas al objeto  
 +\verb Executor  para que sea él quién los lance. En este caso, puesto que sólo se dispone de dos hilos en el pool, sólo se podrán ejecutar 
 +dos hilos en cada momento. Si en algún momento se ocuparán los dos hilos, las demás tareas tendrían que esperar hasta que alguno de los dos 
 +quedará libre para ejecutarse. 
 + 
 +Finalmente se ejecuta el método \verb shutdown() , que hará finalizar el \emph{Executor} una vez hayan terminado todas las tareas que se 
 +encuentran ejecutándose con él. 
 + 
 +Hay que tener en cuenta que existe también un método \verb shutdownNow()  que hace que el \verb Executor  finalice devolviendo todas las 
 +tareas que estaban esperando y no llegaron a ejecutarse. Se esperará que las tareas activas terminen o bien sean interrumpidas. 
 + 
 +\begin{lstlisting}[language=java] 
 +// Crea un executor con dos hilos 
 +ExecutorService executor = Executors.newFixedThreadPool(2); 
 + 
 +// Encola tareas a ajecutar 
 +executor.execute(new Tarea()); 
 +executor.execute(new Tarea()); 
 +executor.execute(new Tarea()); 
 +executor.execute(new Tarea()); 
 + 
 +// Finaliza el executor. Terminará cuando todas las tareas que tiene 
 +// se acaben de forma natural 
 +executor.shutdown(); 
 +\end{lstlisting} 
 + 
 +También es posible trabajar con un \verb ExecutorService  que simplemente cuente con un hilo 
 + 
 +\begin{lstlisting}[language=java] 
 +// Crea un executor con dos hilos 
 +ExecutorService executor = Executors.newSingleThreadExecutor(); 
 +executor.execute(new Tarea()); 
 +. . . 
 +\end{lstlisting} 
 + 
 +En el siguiente caso, instanciamos un pool de hilos planificados, de forma que podremos lanzar las tareas para que comiencen tras un tiempo de 
 +retraso asignado en su lanzamiento a través del método \verb schedule() : 
 + 
 +\begin{lstlisting}[language=java] 
 +// Crea un executor planificado con dos hilos 
 +ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); 
 + 
 +// Encola tareas a ajecutar añadiendo retraso 
 +// en el lanzamiento de cada una de ellas 
 +executor.schedule(new Tarea(), 1, TimeUnit.SECONDS); 
 +executor.schedule(new Tarea(), 2, TimeUnit.SECONDS); 
 +executor.schedule(new Tarea(), 3, TimeUnit.SECONDS); 
 +executor.schedule(new Tarea(), 4, TimeUnit.SECONDS); 
 + 
 +// Finaliza el executor. Terminará cuando todas las tareas que tiene 
 +// se acaben de forma natural 
 +executor.shutdown(); 
 +\end{lstlisting} 
 + 
 +En cualquier caso, podemos encontrarnos con dos tipos de pools de hilos: 
 + 
 +\begin{itemize} 
 +    \item \textbf{Fixed} Se pueden crear a través de los métodos \verb Executors.newFixedThreadPool(...)  y crean un pool de hilos fijo (de 
 +    número determinado) que se reutilizan a medida que van quedando libres. En ningún caso se crearán más hilos de los fijados al crear el 
 +    pool. Si todos los hilos están ocupados y se encola una nueva tarea, ésta tendrá que esperar hasta que un hilo quede libre para 
 +    ejecutarla. 
 +    \item \textbf{Cached} Se pueden crear a través de los métodos \verb Executors.newCachedThreadPool(...)  y crear un pool de hilos no fijo 
 +    (sin número determinado) que se pueden reutilizar si quedan libres. En este caso, si todos los hilos están ocupados y se encola una 
 +    nueva tarea se creará un nuevo hilo para poder ejecutarla. Los hilos que no hayan sido reutilizados en un tiempo determinado (6 
 +    segundos) serán finalizados y eliminados del pool. 
 +\end{itemize} 
 + 
 +\section{Callable y Future} 
 + 
 +Hasta el momento hemos trabajado con objetos \verb Runnable  como objetos que se pueden asignar a un hilo para ejecutar su código en segundo 
 +plano. Los objetos \verb Runnable  no son capaces de devolver ningún valor, simplemente ejecutan el código (el método \verb run()  no 
 +devuelve nada). Tampoco pueden lanzar excepciones puesto que el método \verb run()  de \verb Runnable  no está prearado para ello. 
 + 
 +Java proporciona también una interface llamada \verb Callable  que permite crear objetos que pueden ser asignados a hilos para ejecutarse en 
 +segundo plano, y que además permite que éstas tareas devuelvan un resultado al finalizar y que lancen una excepción. 
 + 
 +\begin{lstlisting}[language=java] 
 +public class TareaCallable implements Callable<Boolean>
 +    @Override 
 +    public Boolean call() throws Exception { 
 +        // Realiza algo 
 +        return true; 
 +    } 
 +
 +\end{lstlisting} 
 + 
 +Ligado a este nuevo tipo de tarea \verb Callable  aparece un nuevo tipo llamado \verb Future  que es el tipo de dato en el que estas nuevas 
 +tareas devolverán su valor. Puesto que ahora tenemos una tarea en segundo plano que debe devolver un valor al finalizar, necesitamos, de 
 +alguna manera, esperar y recoger ese valor cuando corresponda. Así, un objeto \verb Future  es capaz de almacenar el valor de devolución de 
 +un método que ha sido lanzado pero todavía no ha terminado. 
 + 
 +Veamos ahora un ejemplo lanzando una tarea \verb Callable  y cómo se recoge su resultado: 
 + 
 +\begin{lstlisting}[language=java] 
 +// Crea un executor con dos hilos 
 +ExecutorService executor = Executors.newFixedThreadPool(2); 
 + 
 +// Los Callable se ejecutan con el método submit() 
 +Future<Boolean> futuro = executor.submit(new TareaCallable()); 
 + 
 +// Podemos seguir ejecutando código en el hilo principal 
 +System.out.println("El programa sigue funcionando . . ."); 
 + 
 +// Recogemos el resultado de la tarea Callable 
 +// Hay que tener en cuenta que la llamada al método get() 
 +// bloquea la ejecución hasta que termine la tarea 
 +Boolean resultado = futuro.get(); 
 +System.out.println("El resultado de la tarea es: " + resultado); 
 + 
 +// Finaliza el executor. Terminará cuando todas las tareas que tiene 
 +// se acaben de forma natural 
 +executor.shutdown(); 
 +\end{lstlisting} 
 + 
 +Hay que tener en cuenta que la llamada al método \verb get()  bloquea la ejecución del hilo principal hasta que la tarea termine y devuelva 
 +el resultado. La clave aqui está en que unas líneas antes ya tenemos el objeto \verb Future  y podemos seguir ejecutando instrucciones hasta 
 +que realmente necesitemos utilizar el valor, que será cuando invoquemos al método \verb get() . 
 + 
 +Además, siempre podremos cancelar el objeto \verb Future  e incluso comprobar si ha terminado para decidir hacer algo mientras la tarea no 
 +lo haya hecho. De esa manera, una vez lanzada la tarea y obtenido el futuro, podemos ver si procede o no acabar obteniendo el valor que 
 +devuelva. En el siguiente caso, vamos a suponer que a partir de un cierto tiempo de ejecución se cancela la tarea y el programa sigue. 
 +Modificamos la tarea \verb TareaCallable  para forzar a que dure un tiempo determinado y que asi el ejemplo tenga cierta duración y podamos 
 +comprobar como espera (o no) en función del tiempo de //timeout// que le hayamos asignado, o bien utilizando otro criterio cualquiera. 
 + 
 +\begin{lstlisting}[language=java] 
 +public class TareaCallable implements Callable<Boolean>
 +    @Override 
 +    public Boolean call() throws Exception { 
 +        // Realiza algo 
 +        return true; 
 +    } 
 +
 +\end{lstlisting} 
 + 
 +Y el programa principal quedaría asi: 
 + 
 +\begin{lstlisting}[language=java] 
 +// Crea un executor con dos hilos 
 +ExecutorService executor = Executors.newSingleThreadExecutor(); 
 + 
 +long tiempoPasado = 0; 
 +long tiempoInicio = System.currentTimeMillis(); 
 +// Los Callable se ejecutan con el método submit() 
 +Future<Boolean> futuro = executor.submit(new TareaCallable()); 
 + 
 +// Podemos seguir ejecutando código en el hilo principal 
 +System.out.println("El programa sigue funcionando . . ."); 
 + 
 +// Ejecutamos mientras la tarea no haya terminado 
 +while (!futuro.isDone()) { 
 +    try { 
 +        System.out.println("Esperando a que termine la tarea o haciendo algo mientras"); 
 +        Thread.sleep(300); 
 +        // Vamos sumando el tiempo de espera 
 +        tiempoPasado = System.currentTimeMillis() - tiempoInicio; 
 + 
 +        // 4000 hace de timeout. Pasado ese tiempo cancelamos la tarea 
 +        // del segundo plano 
 +        if (tiempoPasado > 4000) { 
 +            futuro.cancel(true); 
 +        } 
 +    } catch (InterruptedException ie) { 
 +        ie.printStackTrace(); 
 +    } 
 +
 + 
 +// Llegados a este punto podemos saber si la tarea fue cancelada 
 +if (futuro.isCancelled()) { 
 +    System.out.println("La tarea fue cancelada"); 
 +
 +// o bien si terminó correctamente 
 +else { 
 +    // Recogemos el resultado de la tarea Callable 
 +    try { 
 +        Boolean resultado = futuro.get(); 
 +        System.out.println("El resultado de la tarea es: " + resultado); 
 +    } catch (ExecutionException|InterruptedException e) { 
 +        e.printStackTrace(); 
 +    } 
 +
 + 
 +// Finaliza el executor. Terminará cuando todas las tareas que tiene 
 +// se acaben de forma natural 
 +executor.shutdown(); 
 +\end{lstlisting} 
 + 
 +En cualquier caso, el método \verb get()  puede ser invocado para establecer un tiempo de \emph{timeout}, a partir del cual el método 
 +devolvería una excepción \verb TimeoutException . Es un caso muy útil cuando, desde nuestra aplicación, invocamos a algún servicio remoto 
 +del que no podemos garantizar su disponibilidad en todo momento. 
 + 
 +\begin{lstlisting}[language=java] 
 +. . . 
 +// Espera 10 segundos antes de lanzar la excepción TimeoutException 
 +Boolean resultado = futuro.get(10, TimeUnit.SECONDS); 
 +. . . 
 +\end{lstlisting} 
 + 
 +Además, es posible lanzar varias tareas al mismo tiempo con el método \verb executeAll  de la clase \verb ExecutorService : 
 + 
 +\begin{lstlisting}[language=java] 
 +. . . 
 +List<Callable<Boolean>> listaTareas = . . . 
 +List<Future<Boolean>> futuros = excecutor.invokeAll(listaTareas); 
 +. . . 
 +for (Future<Boolean> futuro : futuros) { 
 +    System.out.println("Resultado :" + futuro.get()); 
 +
 +. . . 
 +\end{lstlisting} 
 + 
 +\section{Sincronización de hilos} 
 + 
 +\subsection{Monitores} 
 + 
 +\subsection{Semáforos} 
 + 
 +\subsection{CountDownLatch} 
 + 
 +\subsection{CyclicBarrier} 
 + 
 +\section{Colecciones sincronizadas} 
 + 
 +\section{Objetos atómicos} 
 + 
 +\section{Objetos volatile} 
 + 
 +\section{Streams paralelos} 
 + 
 +\section{Patrón Observer} 
apuntes/concurrencia.txt · Last modified: 2023/05/28 23:59 by Santiago Faci