Programación

1º DAM/DAW - Curso 2023-2024

User Tools

Site Tools


apuntes:concurrencia

This is an old revision of the document!


Concurrencia

Programación multihilo

\part*{Programación concurrente}

\section{Introducción a la programación multihilo}

\subsection{Multiproceso}

El multiproceso consiste en la ejecución de varios procesos diferentes de forma simultánea para la realización de una o varias tareas relacionadas o no entre sí. En este caso, cada uno de estos procesos es una aplicación independiente. El caso más conocido es aquel en el que nos referimos al Sistema Operativo (Windows, Linux, MacOS, . . .) y decimos que es multitarea puesto que es capaz de ejecutar varias tareas o procesos (o programas) al mismo tiempo.

\subsection{Multihilo}

Hablamos de multihilo cuando se ejecutan varias tareas relacionadas o no entre sí dentro de una misma aplicación. En este caso no son procesos diferentes sino que dichas tareas se ejecutan dentro del mismo proceso del Sistema Operativo. A cada una de estas tareas se le conoce como hilo o thread (en algunos contextos también como procesos ligeros).

En ambos casos estaríamos hablando de lo que se conoce como Programación Concurrente. Hay que tener en cuenta que en ninguno de los dos casos la ejecución es realmente simultánea, ya que el Sistema Operativo es quién hace que parezca así, pero los ejecuta siguiendo lo que se conoce como algoritmos de planificación.

\subsection{Algoritmos de planificación}

En entornos multitarea, un algoritmo de planificación indica la forma en que el tiempo de procesamiento debe repartirse entre todas las tareas que deben ejecutarse en un momento determinado. Existen diferentes algoritmos de planificación, cada uno con sus ventajas e inconvenientes, pero todos intentan cumplir con los siguientes puntos:

\begin{itemize}

  \item Debe ser imparcial y eficiente
  \item Debe minimizar el tiempo de respuesta al usuario, sobre todo en aquellos procesos o tareas más interactivas
  \item Debe ejecutar el mayor número de procesos
  \item Debe mantener un equilibrio en el uso de los recursos del sistema

\end{itemize}

\subsection{Programación concurrente, paralela y distribuida}

\subsubsection*{Programación concurrente}

Es la programación de aplicaciones capaces de realizar varias tareas de forma simultánea utilizando hilos o threads. En este caso todas las tareas compiten por el uso del procesador (lo más habitual es disponer sólo de uno) y en un instante determinado sólo una de ellas se encuentra en ejecución. Además, habrá que tener en cuenta que diferentes hilos pueden compartir información entre sí y eso complica mucho su programación y coordinación.

\subsubsection*{Programación paralela}

Es la programación de aplicaciones que ejecutan tareas de forma paralela, de forma que no compiten por el procesador puesto que cada una de ellas se ejecuta en uno diferente. Normalmente buscan resultados comunes dividiendo el problema en varias tareas que se ejecutan al mismo tiempo.

\subsubsection*{Programación distribuida}

Es la programación de aplicaciones en las que las tareas a ejecutar se reparten entre varios equipos diferentes (conectados en red, a los que llamaremos nodos). Juntos, estos equipos, forman lo que se conoce como un Sistema Distribuido, que busca formar redes de equipos que trabajen con un fin común

\begin{figure}[h!]

  \centering
  \includegraphics[scale=0.8]{partes/varios/concurrencia.jpg}
  \caption{Programación concurrente/paralela}

\end{figure}

\begin{figure}[h!]

  \centering
  \includegraphics[scale=1]{partes/varios/distribuida.jpg}
  \caption{Programación distribuida}

\end{figure}

\subsection{¿Qué son los hilos?}

Un hilo o thread es cada una de las tareas que puede realizar de forma simultánea una aplicación. Por defecto, toda aplicación dispone de un único hilo de ejecución, al que se conoce como hilo principal. Si dicha aplicación no despliega ningún otro hilo, sólo será capaz de ejecutar una tarea al mismo tiempo en ese hilo principal.

Así, para cada tarea adicional que se quiera ejecutar en esa aplicación, se deberá lanzar un nuevo hilo o thread. Para ello, todos los lenguajes de programación, como Java, disponen de una API para crear y trabajar con ellos.

En cualquier caso, es muy importante conocer los estados en los que se pueden encontrar un hilo. Estos estados se suelen representar mediante un gráfico como el que sigue:

\subsubsection*{Estados de un hilo}

\begin{figure}[h!]

  \centering
  \includegraphics[scale=1.4]{partes/varios/estados_hilo.png}
  \caption{Estados de un hilo}

\end{figure}

\section{Programación multihilo en Java}

\subsection{Creación de un hilo}

Para la creación de hilos en Java disponemos de varias vías, combinando el uso de la clase Thread y el interface Runnable según nos interese:

\begin{itemize}

  \item Podemos utiliza la clase Thread heredando de ella. Es quizás la forma más cómoda porque una clase que hereda de Thread se convierte automáticamente en un hilo. Tiene una pega: esa clase ya no podrá heredera de ninguna otra, por lo que si la arquitectura de nuestra aplicación lo requiere ya no podríamos.
  \item Si tenemos la limitación que acabamos de comentar para el primer caso, podemos implementar el interface Runnable de forma que la clase que nosotros estamos implementado podrá además heredar sin ninguna limitación. Sólo cambia un poco la forma de trabajar directamente con la clase hilo.
  \item Por otra parte también podemos crear un hilo utilizando una clase anónima. No es un método que se recomiende pero en algunos casos, cuando la clase que hace de hilo no va a tener una estructura concreta es bastante cómodo hacerlo de esta manera.

\end{itemize}

\subsubsection*{Crear un hilo heredando de la clase Thread}

\begin{lstlisting}[language=java] public class Tarea extends Thread {

@Override
public void run() {
  for (int i = 0; i < 10; i++) {
    System.out.println("Soy un hilo y esto es lo que hago");
  }
}

}

. . .

public class Programa {

public static void main(String args[]) {
  Tarea tarea = new Tarea();
  tarea.start();
  System.out.println("Yo soy el hilo principal y sigo haciendo mi trabajo");
  System.out.println("Fin del hilo principal");
}

} \end{lstlisting}

\subsubsection*{Crear un hilo implementando la interfaz Runnable}

\begin{lstlisting}[language=java] public class OtraClase {

. . .
. . .

}

. . .

public class Tarea extends OtraClase implements Runnable {

@Override
public void run() {
  for (int i = 0; i < 10; i++) {
    System.out.println("Soy un hilo y esto es lo que hago");
  }
}

}

. . .

public class Programa {

public static void main(String args[]) {
  Tarea tarea = new Tarea();
  Thread hilo = new Thread(tarea);
  hilo.start();
  System.out.println("Yo soy el hilo principal y sigo haciendo mi trabajo");
  System.out.println("Fin del hilo principal");
}

} \end{lstlisting}

\subsubsection*{Crear un hilo implementando una clase anónima/expresión lambda}

\begin{lstlisting}[language=java] public class Programa {

public static void main(String args[]) {

Thread hilo = new Thread(new Runnable() {

    @Override
    public void run() {
      for (int i = 0; i < 10; i++) {
      System.out.println("Soy un hilo y esto es lo que hago");
    }
  });

hilo.start();

  System.out.println("Yo soy el hilo principal y sigo haciendo mi trabajo");
  System.out.println("Fin del hilo principal");
}

} \end{lstlisting}

En cualquier caso tenemos que tener siempre en cuenta las siguientes consideraciones:

\begin{itemize}

  \item Siempre se debe sobreescribir (Override) el método run() e implementar allí lo que tiene que hacer el hilo
  \item Podemos hacer que el hilo haga un número finito de cosas o bien que esté siempre en segundo plano (tendremos que asegurar que el método run() se ejecuta de forma continuada)(¿cómo se hace eso?)
  \item Los problemas vienen cuando existen varios hilos. Hay que tener en cuenta que pueden compartir datos y código y encontrarse en diferentes estados de ejecución
  \item La ejecución de nuestra aplicación será thread-safe si se puede garantizar una correcta manipulación de los datos que comparten los hilos de la aplicación sin resultados inesperados (más adelante veremos cómo)
  \item Además, en el caso de aplicaciones multihilo, también nos puede interesar sincronizar y comunicar unos hilos con otros

\end{itemize}

También resulta interesante saber cómo detener un hilo. En este caso, la API de Java desaconsejó el método stop() que en un principio ideó para detener la ejecución. Así, hoy en día, se nos anima a que seamos nosotros quienes implementemos formas limpias de detener nuestros hilos.

\subsection{Sincronización de hilos}

El API de Java proporciona una serie de métodos en la clase Thread para la sincronización de los hilos en una aplicación:

\begin{itemize}

  \item \verb join()  Se espera la terminación del hilo que invoca a este método antes de continuar
  \item \verb Thread.sleep(int)  El hilo que ejecuta esta llamada permanece dormido durante el tiempo especificado como parámetro (en ms)
  \item \verb isAlive()  Comprueba si el hilo permanece activo todavía (no ha terminado su ejecución)
  \item \verb yield()  Sugiere al scheduler que sea otro hilo el que se ejecute (no se asegura)

\end{itemize}

\begin{lstlisting}[language=java] public static void main(String args[]) {

Hilo hilo1 = new Thread(new Tarea());
Hilo hilo2 = new Thread(new Tarea());

hilo1.start();

hilo2.start();

. . .

. . .

hilo1.join();

hilo2.join();

System.out.println(“Fin de la ejecución de los dos hilos”); } \end{lstlisting}

El hilo principal espera a que ambos hilos se hayan ejecutado para continuar (o para lo que sea)

\begin{lstlisting}[language=java] public static void main(String args[]) {

Hilo hilo1 = new Thread(new Tarea());
Hilo hilo2 = new Thread(new Tarea());

hilo1.start();

hilo1.join();

hilo2.start();

hilo2.join();

System.out.println(“Fin de la ejecución de los dos hilos”); } \end{lstlisting}

En este caso los hilos se ejecutan uno después de otro

\begin{lstlisting}[language=java] public class Tarea implements Runnable {

@Override
public void run() {
  for (int i = 0; i < 10; i++) {
    System.out.println("Soy un hilo y esto es lo que hago");
    try {
      Thread.sleep(500);
    } catch (InterruptedException ie) {
      ie.printStackTrace();
    }
  }
}

} \end{lstlisting}

En este caso el hilo duerme (detiene su ejecución) durante el tiempo especificado (en ms). Durante ese momento podrán ejecutarse otros hilos

\begin{lstlisting}[language=java] public class TareaPrincipal implements Runnable {

@Override

public void run() {
  for (int i = 0; i < 10; i++) {
    System.out.println("Soy la TarePrincipal");
    try {
      Thread.sleep(500);
    } catch (InterruptedException ie) {
      ie.printStackTrace();
    }
  }
}

}

. . .

public class TareaAlive implements Runnable {

private Thread otroHilo;

public TareaAlive(Thread otroHilo) {

  this.otroHilo = otroHilo;
}

@Override

public void run() {
  while (otroHilo.isAlive()) {
    System.out.println("Yo hago cosas mientras el otro hilo siga en ejecución");
    try {
      Thread.sleep(500);
    } catch (InterruptedException ie) {
      ie.printStackTrace();
    }
  }

System.out.println(“El otro hilo ha terminado. Yo también”);

}

}

. . .

public class Programa {

public static void main(String args[]) {
  TarePrincipal tareaPrincipal = new TareaPrincipal();
  Thread hiloPrincipal = new Thread(tareaPrincipal);

TareaAlive tareaAlive = new TareaAlive(hiloPrincipal);

  Thread hiloAlive = new Thread(tareaAlive);

hiloPrincipal.start();

  hiloAlive.start();

System.out.println(“Se han terminado los dos hilos?”);

}

} \end{lstlisting}

\verb isAlive() está indicando que el hilo está vivo (ha iniciado su ejecución y aún no ha muerto, puede estar en cualquier estado intermedio, incluso durmiendo)

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.1615382364.txt.gz · Last modified: 2021/03/10 13:19 by Santiago Faci