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.
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.
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:
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.
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.
En este algoritmo, de todos los procesos listos para ser ejecutados, lo hará primero el más corto
De todos los procesos listos para ejecución, se ejecutará aquel al que le quede menos tiempo para terminar.
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, . . .
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.
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.
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)
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:
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:
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.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.
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"); } }
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"); } }
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:
Override
) el método run()
e implementar allí lo que tiene que hacer el hilorun()
se ejecuta de forma continuada)(¿cómo se hace eso?)
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:
join()
Se espera la terminación del hilo que invoca a este método antes de continuarThread.sleep(int)
El hilo que ejecuta esta llamada permanece dormido durante el tiempo especificado como parámetro (en ms)isAlive()
Comprueba si el hilo permanece activo todavía (no ha terminado su ejecución)yield()
Sugiere al scheduler que sea otro hilo el que se ejecute (no se asegura)
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.
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)
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
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
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)
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
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(); . . . }
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(); . . . }
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
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
<?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)
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
© 2016-2021 Santiago Faci