Table of Contents

Programación multihilo

Introducción a la programación multiproceso y multihilo

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.

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.

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:

FCFS: First Come First Served

El primer proceso que llegue al procesador se ejecuta antes y de forma completa. Hasta que su ejecución no termina no podrá pasarse a ejecutar otro proceso.

RR: Round Robin

Se le conoce también como algoritmo de turno rotatorio. En este caso se designa una cantidad corta de tiempo (quantum) de procesamiento a todas las tareas. Las que necesiten más tiempo de proceso deberán esperar a que vuelva a ser su turno para seguir ejecutándose.

SPF: Shortest Process First

En este algoritmo, de todos los procesos listos para ser ejecutados, lo hará primero el más corto

SRT: Shortest Remaining Time

De todos los procesos listos para ejecución, se ejecutará aquel al que le quede menos tiempo para terminar.

Varias colas con realimentación

Es un algoritmo más complejo que todos los anteriores y, por tanto, más realista. Se utiliza en entornos donde se desconoce el tiempo de ejecución de un proceso al inicio de su ejecución. En este caso, el sistema dispone de varias colas que a su vez pueden disponer de diferentes políticas unas de otras. Los procesos van pasando de una cola a otra hasta que terminan su ejecución. En algunos casos, el algoritmo puede adaptarse modificando el número de colas, su política, . . .

multinivel.jpg
Figure 1: Algoritmo varias colas con realimentación

Ejercicios

  1. Escribe un programa en Java que atienda una serie de tareas usando un algoritmo FCFS. Muestra las tareas que se van realizando en cada momento y el tiempo en que se inicia y termina cada una
  2. Escribe ahora el mismo programa pero usando un algoritmo RR
  3. Escribe ahora el mismo programa pero usando un algoritmo SRT

Programación concurrente, paralela y distribuida

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.

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.

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 1)

concurrencia.jpg
Figure 2: Programación concurrente / paralela
distribuida.jpg
Figure 3: Programación distribuida

¿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:

Estados de un hilo

Figure 4: Estados de un hilo

Programación multihilo en Java

Creación y ejecució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:

Crear un hilo heredando de la clase Thread

En este caso, la clase Tarea se conviene automáticamente en un hilo por el mero hecho de heredar de Thread. Sólo tenemos que tener en cuenta que, al heredar de esta clase, tenemos que implementar el método run() y escribir en él el código que queremos que esta clase ejecute cuando se lance como un hilo con el método start() (que también hereda de Thread)

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");
  }
}

Crear un hilo implementado el interfaz Runnable

En este caso, suponemos que necesitamos que nuestra clase hilo herede de una segunda clase. En este caso, la clase deberá además implementar el interfaz Runnable y, como en el primer caso, implementar el método run() con la misma idea que en el punto anterior. Más adelante, tendremos que crear un objeto directamente de la clase Thread y pasarle como parámetro al constructor un objeto de nuestra clase hilo. De esa manera, el objeto Thread será un hilo que se comportará como el método run() de nuestra clase Tarea haya definido.

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");
  }
}

Crear un hilo implementado una clase anónima

Por último, implementar una clase anónima también permite crear hilos aunque sólo se recomienda para casos en los que la clase que se convierte en un hilo no tenga una estructura muy compleja ya que quedaría un código bastante ilegible.

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");
  }
}

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





Ejercicios

  1. ¿Qué pasa si ejecutas varias veces el código de los ejemplos? ¿Siempre ocurre lo mismo?
  2. ¿Existe alguna manera de asegurar que todo se va a ejecutar en un orden concreto?

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:

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 se 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.

join() I

public static void main(String args[]) {
  Hilo hilo1 = new Thread(new Tarea());
  Hilo hilo2 = new Thread(new Tarea());
 
  hilo1.start();
  hilo2.start();
 
  . . .
  . . .
 
  try {
    hilo1.join();
    hilo2.join();
  } catch (InterruptedException ie) {
    ie.printStackTrace();
  }
 
  System.out.println("Fin de la ejecución de los dos hilos");
}

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


Ejercicios

  1. Prueba a ejecutar este código sin las llamadas al método join() de ambos hilos (y comprueba el resultado)

join() II

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");
}

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


Ejercicios

  1. ¿Qué pasa si eliminamos las llamadas al método join() en ambos casos?
  2. Prueba a hacerlo con más hilos. ¿Y con n hilos?

Thread.sleep

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();
      }
    }
  }
}

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

isAlive()

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?");
  }
}

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)


Ejercicios

  1. ¿Podemos asegurar el resultado de la aplicación anterior?
  2. ¿Y si lo hacemos extendiendo de la clase Thread a la hora de implementar los hilos?



Utilización de hilos en GUIs (Graphical User Interfaces) con Swing

En ocasiones, en aplicaciones con GUIs, el usuario debe esperar a que una tarea que se ejecuta en segundo plano termine. Mientras tanto, es muy habitual notificar el estado de la misma durante su ejecución o bien cuando termine. Esa notificación se puede hacer de muchas maneras, usando etiquetas de texto, barras de progreso, ventanas emergentes u otros recursos.

Puesto que Java “pinta” la GUI en la pantalla utilizando un hilo, necesitamos que todas las tareas que ejecutan operaciones sobre esa GUI lo hagan dentro del mismo hilo. Si dos hilos diferentes actúan sobre un mismo componente gráfico pueden darse situaciones no previstas. Es por eso que Java tiene en su API la clase SwingWorker, destinada para la creación de tareas en segundo plano que interactúan con el GUI de la aplicación. Así, cuando los hilos de la aplicación se ejecutan sin comportamientos inesperados podemos decir que lo hacen de forma thread-safe.

La clase SwingWorker

La clase SwingWorker pertenece al API de Java y permite ejecutar tareas sobre GUIs de forma thread-safe, permitiendo que éstas corran en el mismo hilo que el que Java hace funcionar para todos los controles Swing (Event Dispatch Thread).

En aplicaciones con GUIs varios hilos pueden actuar sobre el mismo elemento gráfico (una etiqueta de texto, por ejemplo) por lo que varios hilos compartirán ese recurso. Así, la clase SwingWorker garantiza que el acceso a ese recurso compartido se hace de forma thread-safe.

Desde el punto de vista de la implementación, además de codificar qué queremos que haga esta tarea, podremos devolver el resultado (si procede) y también notificar el avance de la misma (si procede, en una barra de progreso, por ejemplo). Además, también podremos implementar un PropertyChangeListener para actualizar el estado de la GUI de la aplicación mientras se ejecuta esta tarea en el segundo plano.

public class Tarea extends SwingWorker <ArrayList <BufferedImage>, Integer> { 
  private JProgressBar barraProgreso;
  // Se pasa como parametro la barra de progreso donde se quiere notificar
  public Tarea(JProgressBar barraProgreso) { 
    this.barraProgreso = barraProgreso;
  }
 
  @Override
  public ArrayList <BufferedImage > doInBackground() throws Exception {
    // Carga unas imagenes del disco duro a un Array y devuelve la lista
    . . .
    // Se notifica el avance (valor entre 0 y 100)
    setProgress(avanceCarga); 
  }
 
  @Override
  public void process(List<Integer> valores) {
    // Aqui se reciben los valores del metodo publish()
    // Es una lista puesto que a vece se envian varios 
    // en una sola llamada 
    barraProgreso.setValue(valores.get(0));
  } 
}
 
public class ProgramaGUI { 
  . . .
  // Lanza la ejecucion de la tarea en segundo plano
  Tarea tarea = new Tarea(barraProgreso); 
  tarea.execute();
  . . .
  // Obtiene el resultado de la carga de imagenes 
  ArrayList<BufferedImage> resultado = tarea.get(); 
  . . .
}

Property Change Listener

public class Tarea extends SwingWorker <ArrayList <BufferedImage>, Integer> {
 
  @Override
  public ArrayList <BufferedImage > doInBackground() throws Exception {
    // Carga unas imagenes del disco duro a un Array y devuelve la lista
    . . .
    // Se notifica el avance a traves del Property Change Listener
    setProgress(avanceCarga); }
 
  @Override
  public void done () {
    // Se notifica el final de la carga a traves del Property Change Listener
    firePropertyChange("fin", false, true); 
  }
}
 
public class ProgramaGUI {
  . . .
  Tarea tarea = new Tarea();
  tarea.addPropertyChangeListener(new PropertyChangeListener() {
    @Override
    public void propertyChange(PropertyChangeEvent event) {
      if (event.getPropertyName().equals("progress")) {
        int valor = (Integer) event.getNewValue();
        // Pintar este valor en barra de progreso o similar
      }
      else if (event.getPropertyName().equals("fin")) {
        // Fin de la carga de las imagenes
        // Hacer algo
      }
    }
  });
  tarea.execute();
  . . .
}



Utilización de hilos en GUIs (Graphical User Interfaces) con JavaFX

public class MyTask extends Task<Integer> {
    public MyTask() {
    }
 
    @Override
    public Integer call() throws Exception {
        . . .
        . . .
    }
}
. . .
public ProgressBar progressBar;
public Label statusLabel;
private MyTask myTask;
. . .
. . .
@FXML
public void startTask(Event event) {
    . . .
   myTask = new MyTask();
 
    pbProgress.progressProperty().unbind();
    pbProgress.progressProperty().bind(downloadTask.progressProperty());
 
    myTask.stateProperty().addListener((observableValue, oldState, newState) -> {
        if (newState == Worker.State.SUCCEEDED) {
            Alert alert = new Alert(Alert.AlertType.INFORMATION);
            alert.setContentText("La tarea ha terminado");
            alert.show();
        }
    });
 
    myTask.messageProperty().addListener(
        (observableValue, oldValue, newValue) -> statusLabel.setText(newValue));
    new Thread(myTask).start();
}
. . .
. . .
@FXML
public void stopTask(Event event) {
    if (myTask != null)
        myTask.cancel();
}
. . .
. . .

Ver ejemplo multidescarga en GitHub


Ejercicios

  1. Escribe una aplicación que sirva de Alarma. El usuario podrá fijar una hora en la que tendrá que saltar un mensaje. En cualquier momento, podrá cancelarse la alarma
  2. ¿Cómo harías la aplicación anterior para que se puedan fijar varias Alarmas simultáneas a distintas horas?
  3. ¿Y si necesitas que una aplicación compruebe algo a intervalos regulares de tiempo? Por ejemplo, si hay actualizaciones, auto-guardar un documento, . . .
  4. Realiza una aplicación con JavaFX que sirva para descargar ficheros (Descargar ficheros con Java)

Ficheros de Registro (Logs)

A continuación se muestra, utilizando la librería log4j, un ejemplo de aplicación donde se realizan una serie de trazas a lo largo de su ejecución. La aplicación está compuesta por las dos clases que se muestran a continuación, con el objetivo de mostrar la traza cuando son varias clases las que ejecutan código.

public class Aplicacion {
 
  private static final Logger logger = LogManager.getLogger(Aplicacion.class);
 
  public static void main(String args[]) {
 
    // Diferentes niveles de traza
    logger.trace("Aplicación iniciada");
    logger.error("Error de algo");
    logger.trace("Aplicación finalizada");
    logger.debug("Información para depurar");
    logger.warn("Esto es un aviso");
 
    OtraClase unObjeto = new OtraClase();
    unObjeto.unMetodo();
 
    try {
      // Forzamos una excepción para registrar su traza con log4j
      int x = 5 / 0;
    } catch (Exception e) {
      logger.trace("Se ha producido una excepción");
 
      // Almacena la traza de la excepción como String y lo registra con log4j
      StringWriter sw = new StringWriter();
      e.printStackTrace(new PrintWriter(sw));
      logger.error(sw.toString());
      }
  }
}
public class OtraClase {
 
  private static final Logger logger = LogManager.getLogger(OtraClase.class);
 
  public void unMetodo() {
 
    logger.trace("Se ha ejecutado el método unMetodo");
  }
}

Antes de poder ejecutar la aplicación, se ha creado un fichero mínimo de configuración para log4j creando el siguiente fichero XML donde se habilita la traza por Consola y Fichero con un patrón de mensaje determinado

log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
  <Appenders>
    <Console name="Console" target="SYSTEM_OUT">
      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
    </Console>
    <File name="Fichero" fileName="ejemplolog4j.log" bufferedIO="false" advertiseURI="file://ejemplolog4j.log" advertise="true">
      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
    </File>
  </Appenders>
  <Loggers>
    <Root level="trace">
      <AppenderRef ref="Console"/>
      <AppenderRef ref="Fichero"/>
    </Root>
  </Loggers>
</Configuration>

Así, para el ejemplo anterior, la traza resultante (tanto para consola como para el fichero ejemplolog4j.log) sería la siguiente

10:46:27.049 [main] TRACE com.sfaci.ejemplolog4j.Aplicacion - Aplicación iniciada
10:46:27.051 [main] ERROR com.sfaci.ejemplolog4j.Aplicacion - Error de algo
10:46:27.051 [main] TRACE com.sfaci.ejemplolog4j.Aplicacion - Aplicación finalizada
10:46:27.051 [main] DEBUG com.sfaci.ejemplolog4j.Aplicacion - Información para depurar
10:46:27.051 [main] WARN  com.sfaci.ejemplolog4j.Aplicacion - Esto es un aviso
10:46:27.051 [main] TRACE com.sfaci.ejemplolog4j.OtraClase - Se ha ejecutado el método unMetodo
10:46:27.052 [main] TRACE com.sfaci.ejemplolog4j.Aplicacion - Se ha producido una excepción
10:46:27.052 [main] ERROR com.sfaci.ejemplolog4j.Aplicacion - java.lang.ArithmeticException: / by zero
	at com.sfaci.ejemplolog4j.Aplicacion.main(Aplicacion.java:38)
	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:483)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

Ejercicios

  1. Realiza una aplicación de consola que cuente hasta un número determinado (mostrando la secuencia por pantalla) utilizando dos hilos, de forma que cada uno de ellos cuente un rango de números

  2. Realiza una aplicación de consola que cuente hasta un número determinado (mostrando la secuencia por pantalla) utilizando un número determinado de hilos. La secuencia de números se repartirá a partes iguales entre todos los hilos de forma que a cada uno se le asigne un rango

  3. Realiza una aplicación que simule una carrera de coches (de hasta 4 coches). Para cada coche se podrá configurar su velocidad y en la aplicación podremos configurar la distancia del circuito. Una vez lanzada la carrera se irá mostrando por pantalla (mediante barras de progreso, por ejemplo) el desarrollo de la misma (el avance de cada coche en el tiempo). Al final de la carrera se anunciará el coche ganador y los demás se detendrán mostrando cuánta distancia han recorrido

  4. Realiza una aplicación en la que el usuario pueda programar una cuenta atrás que al terminar muestre un mensaje en la pantalla principal. Además, se mostrará en una barra de progreso el transcurso de dicha cuenta atrás (vaciando la barra de progreso)

  5. Realiza una aplicación en la que se muestre, mediante una barra de progreso y una etiqueta de texto, el tiempo que pasa, en segundos, hasta una cantidad que habrá introducido el usuario. En cualquier momento éste podrá cancelar la cuenta.
  6. Realiza una aplicación que descargue un fichero de Internet mostrando, al final, la duración de la descarga formato MM:SS

Ejercicios examen

  1. Realiza una aplicación en la que se muestre, mediante una barra de progreso y una etiqueta de texto, la cuenta atrás desde una cantidad de segundos introducida por el usuario. En cualquier momento éste podrá cancelar la cuenta:
    1. La tarea se lanzará en segundo plano una vez el usuario pulse el botón
    2. La tarea mantendrá la cuenta atrás de los segundos que el usuario haya introducido
    3. El usuario podrá cancelar la tarea en cualquier momento
    4. Se mostrará el progreso en una barra de progreso y como texto en una etiqueta
    5. Cuando la tarea termine mostrará un mensaje en una ventana emergente
  2. Realiza una aplicación de texto que lance dos hilos de forma que el segundo se ejecute mientras dure la ejecución del primero
  3. Realiza una aplicación que ejecute 4 hilos de forma que se ejecuten de forma ordenada uno detrás de otro esperando cada uno a que termine el anterior para ejecutarse

Proyectos de ejemplo

Todos los proyectos de este tema se pueden encontrar en el repositorio concurrencia y java-javafx de GitHub.

Los proyectos de los ejercicios que se vayan haciendo en clase estarán dispnibles en el repositorio psp-ejercicios de GitHub

Para manejaros con Git recordad que tenéis una serie de videotutoriales en La Wiki de Entornos de Desarrollo


Prácticas


© 2016-2021 Santiago Faci

1)
Por ejemplo, el proyecto SETI@home http://setiathome.berkeley.edu