====== Programación Concurrente =====
===== Concurrencia =====
La concurrencia es la ejecución de diferentes tareas por la CPU de un ordenador, compartiendo los recursos del sistema entre ellas (memoria, ficheros, periféricos, etc). Cuando hablamos de concurrencia podemos hablar de procesos o de hilos.
==== Procesos ====
Un programa se puede definir como un conjunto de instrucciones que persiguen una finalidad, diseñados es un lenguaje de programación. Un **proceso** es un programa en estado de ejecución. Consiste en el código del programa, los datos, un contador de programa, etc.
=== Multiproceso ===
Es la capacidad de ejecutar varios procesos de forma simultánea. Los Sistemas Operativos actuales disponen de mecanismos que permiten la ejecución de muchos procesos. A estos SSOO les llamamos //multitarea//.
Para que los distintos procesos se puedan ejecutar concurrentemente, el sistema operativo debe parar e iniciar los procesos para irles asignando tiempo de ejecución. Cuando se para un proceso para ejecutar otro, se debe guardar el estado del proceso para reanudar su ejecución posteriormente. Cada vez que el sistema detiene un proceso para ejecutar otro, debe guardar su información es una estructura de datos llamado **Bloque de Control de Proceso**. A este proceso se le conoce como **cambio de contexto**:
* Guardar el estado del programa que se estaba ejecutando en SU BCP.
* Seleccionar otro programa para ejecutar mediante un algoritmo de planificación.
* Restaurar el estado del programa seleccionado a partir de su BCP.
* Ejecutar el programa seleccionado.
=== Multihilo ===
Los hilos permiten ejecutar diferentes tareas dentro de un mismo proceso, mejorando el rendimiento de la CPU. En este caso una misma aplicación puede ejecutar diferentes hilos de forma concurrente que se encargan de diferentes tareas. Todos estos hilos comparten recursos del proceso.
==== Algoritmos de planificación ====
En entornos concurrentes, un **algoritmo de planificación** se encarga de elegir qué proceso o hilo se ejecutará a continuación. Un algoritmo de planificació debe:
* Ser imparcial y eficiente
* Minimizar el tiempo de respuesta al usuario, sobre todo en aquellas tareas más interactivas
* Ejecutar el mayor número de procesos en el menor tiempo posible
* Mantener un equilibrio en el uso de los recursos del sistema
=== 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, . . .
----
{{ ejercicio.png?75}}
=== Ejercicios ===
- Escribe un programa en Java que atienda una serie de tareas. Cada tarea será representada mediante un objeto con un nombre y un entero que indica el tiempo que durará la tarea. El programa nos permitirá dar de alta una serie de tareas dando su nombre y su tiempo, y posteriormente ejecutarlas todas seleccionándolas tal y como hacen los siguientes algoritmos. Muestra las tareas que se van realizando en cada momento y el tiempo en que se inicia y termina cada una:
- Usando un algoritmo FCFS.
- Usando un algoritmo RR
- 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 (( Por ejemplo, el proyecto SETI@home http://setiathome.berkeley.edu))
===== Programación multihilo en Java =====
{{ :bloque1:mapa_conceptual.png?450 |}}
A continuación, tenemos un {{ :bloque1:infografia_hilos.png?linkonly |esquema del desarrollo del bloque}}, a través de las diferentes etapas hasta la práctica final.
==== ¿Qué es un hilo? ====
Los hilos o //Threads// son procesos ligeros que se ejecutan simultáneamente en un mismo proceso. La ejecución simultanea de diferentes hilos mejora el rendimiento y la utilización de la CPU ya que podemos realizar varias tareas al mismo tiempo.
Todo programa usa como mínimo un hilo de ejecución que se conoce como //hilo principal//. Si no creamos explicitamente ningún otro hilo, dicho programa solo podrá ejecutar tareas secuencialmente.
Cada lenguaje tiene su propia API para trabajar con hilos; en Java podemos crear hilos implementando la interface ''Runnable'' o heredando de la clase ''Thread''.
En un entorno multihilo, el //planificador de hilos// de Java, que forma parte de la JVM, se encarga de asignar un periodo de tiempo de ejecución a cada hilo de modo que todos puedan ejecutarse concurrentemente.
=== Primer plano ===
Aquí se ejecuta únicamente un hilo llamado "hilo principal". En este hilo hemos programado siempre, sin conocimiento de que estábamos trabajando ya con hilos. También, puede ser usado para hacer cálculos u otros procesamientos complejos, aunque estos deberían de evitarse en este hilo. Puedo comprobar si estoy en el hilo principal:
Thread.currentThread().getName();
=== Segundo plano ===
En inglés //background//, es donde se ejecutan el resto de hilos. El segundo plano tiene la característica de darse en el mismo momento que el primer plano. Estos hilos deben llevar las ejecuciones pesadas de la aplicación. El segundo plano es la ejecución que el usuario no ve, es más, ni le interesa; para el usuario no existe.
==== Implementación ====
Para la creación de hilos en Java disponemos de varias vías, combinando el uso de la clase y el interface según nos interese:
* Heredando de la clase [[https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html|Thread]]
* Implementando la interface [[https://docs.oracle.com/javase/8/docs/api/java/lang/Runnable.html|Runnable]]
* Utilizando una clase Anónima
* Otras formas: expresión //lambda//, ó clase interna
=== Heredando/extendiendo la clase Thread ===
Debemos crear una clase que extienda de ''Thread'' y acordarnos de sobrescribir el método ''run()''. Es la forma más sencilla pero tiene la limitación de que en Java, cada clase solo puede extender de una única clase.
Para iniciar un hilo debemos crear un objeto de nuestra nueva clase y llamar a su método ''start()'' que hereda de la clase Thread.
public class Hilo extends Thread {
private int id;
public Hilo(int id){
this.id = id;
}
@Override
public void run() {
System.out.println("Soy el hilo numero " + id);
}
}
. . .
public class HiloPrincipal {
public static void main(String args[]) {
Hilo hilo = null;
for( int i = 0; i < 3; i++){
hilo = new Hilo(i);
hilo.start();
}
System.out.println("Fin del hilo principal");
}
}
{{ vimeo>775547268?medium }}
> Iniciar y parar hilo: Extendiendo de Thread
=== Implementando la interface Runnable ===
Nos permite seguir extendiendo de otra clase así como implementar otras interfaces. Además, el compilador nos //fuerza// a sobrescribir el método ''run()''.
En este caso debemos crear un objeto ''Thread'' por cada hilo que necesitemos y pasarle nuestro objeto ''Runnable''. Esto nos permite compartir el mismo objeto ''Runnable'' para cada hilo, muy útil cuando necesitamos que diferentes hilos compartan elementos del mismo objeto Runnable.
public class Tarea implements Runnable {
private int id;
public Hilo(int id){
this.id = id;
}
@Override
public void run() {
System.out.println("Soy el hilo numero " + id);
}
}
. . .
public class Programa {
public static void main(String args[]) {
Tarea tarea1 = new Tarea(1);
Thread hilo1 = new Thread(tarea1);
System.out.println("Inicio del Hilo principal");
hilo1.start();
System.out.println("Fin del hilo principal");
}
}
{{ vimeo>775547331?medium }}
> Iniciar y parar hilo: Implementando Runnable
=== Mediante una clase anónima ===
En algunas situaciones puede resultar más cómodo crear un hilo utilizando una clase anónima. Esto se recomienda cuando la clase no tiene una estructura muy compleja.
La ventaja es que, desde el método ''run()'' se puede acceder directamente a miembros de la clase principal, sin necesidad de recibir parámetros.
public class Principal {
public static void main(String args[]) {
Thread hilo = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hilo secundario");
}
});
hilo.start();
}
}
{{ vimeo>775547222?medium }}
> Crear hilo mediante clase anónima
=== Otras formas ===
También se puede utilizar las expresiones //lambda//, o mediante una clase interna (definición de una clase dentro de otra):
//Usando lambdas (a partir de Java 8): similar a clase anónima
Thread hilo = new Thread(() -> {
//Contenido del método run()
System.out.println("Hilo secundario");
});
hilo.start();
//Usando una clase interna
public class Programa{
private int contador;
private String cadena;
. . .
class Hilo implements Runnable{
@Override
public void run(){
System.out.println("soy un hilo y accedo al campo cadena: " + cadena);
}
}
}
===Consideraciones al crear hilos===
* Si heredamos de la clase ''Thread'' cada hilo crea un objeto único asociado. Cuando implementamos ''Runnable'' podemos reutilizar el mismo objeto Runnable con diferentes hilos.
* Siempre se debe sobreescribir (''Override'') el método ''run()'' e implementar allí lo que tiene que hacer el hilo.
* Es habitual hilos cuyo método **run()** se ejecuta repetidamente mediante un bucle hasta que decidamos pararlo de agún modo. En estos casos es recomendable usar ''sleep()'' para no sobrecargar la CPU.
* 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.
* 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.
* Además, en el caso de aplicaciones multihilo, también nos puede interesar sincronizar y comunicar unos hilos con otros.
----
{{ ejercicio.png?75}}
=== Ejercicios ===
- ¿Qué pasa si ejecutas varias veces el código de los ejemplos? ¿Siempre ocurre lo mismo?
- ¿Existe alguna manera de asegurar que todo se va a ejecutar en un orden concreto?
----
==== Clase Thread ====
Contiene una serie de constructores y métodos para manejar hilos. Hay varios [[https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html|métodos que están obsoletos]] y que no debemos utilizar.
^Constructor^Finalidad^
|Thread(Runnable objetoRunnable)|Crea un hilo a partir del objeto Runnable|
|Thread(String name)|Crea un hilo y le da valor a su propiedad name|
|Thread(Runnable objetoRunnable, String name)|Crea un hilo a partir del objeto Runnable y da nombre al hilo|
^Método^Finalidad^
|void start()|Inicia el hilo. La JVM llama al método run()|
|boolean isAlive()|Comprueba si se ha terminado el método run()|
|int getId()|Devuelve el id del hilo|
|String getName()|Devuelve el nombre del hilo|
|void setName(String name)| Asigna un nombre al hilo|
|void join()|Se espera la terminación del hilo que invoca a este método antes de continuar|
|static void sleep(long ms)|Hace que el hilo espere la cantidad de ms|
|static void yield()|Detiene temporalmente el hilo y permite que se ejecute otro|
|static Thread currentThread()| Indica el hilo actual en ejecución|
|void interrupt()| Desbloquea un hilo bloqueado (wait(),sleep(), join(), y activa el flag interupt status|
|static boolean isInterrupted()|Comprueba el estado del flag interrupt status, y lo resetea|
=== Ciclo de vida de un hilo===
En Java, cada hilo de jecución pasa por diferentes estados como vemos en la siguiente imagen:
La clase ''Thread'' tiene el método ''getState()'' el cual devuelve el valor del estado actual del hilo:
* **NEW** – hilo que ha sido creado pero aun no ha sido iniciado (''start()'')
* **RUNNABLE** – hilo en ejecución, o preparado para ser ejecutado. Ha iniciado su método ''run()''
* **TERMINATED** – hilo que ha terminado su método ''run()'' o debido a alguna excepción no capturada.
* **BLOCKED, WAITING, TIMED_WAITING** – hilo esperando a entrar a un bloque //synchronized//, o por llamadas a ''sleep()'', ''wait()'', ''join()''...
====Programación Thread-Safe====
Hace referencia al enfoque que debemos tomar a la hora de programar aplicaciones en las que diferentes hilos comparten el mismo proceso y no genera situaciones inesperadas.
* Debe garantizar una correcta manipulación de los datos que comparten los hilos de la aplicación sin resultados inesperados.
* Plantea una exclusión mutua en el acceso a bloques compartidos.
* Los datos compartidos son accedidos de forma atómica, por solo un hilo al mismo tiempo.
* Sincroniza el órden de acceso a los datos compartidos para evitar condiciones de carrera.
===== 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:
* ''Thread.sleep(int)'' El hilo que ejecuta esta llamada permanece //dormido// durante el tiempo especificado como parámetro (en ms)
* ''join()'' Se espera la terminación del hilo que invoca a este método antes de continuar
* ''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)
==== Método Thread.sleep() ====
En este caso el hilo //duerme// (detiene su ejecución) durante el tiempo especificado (en ms). Durante ese periodo podrán ejecutarse otros hilos. Propaga una excepción de clase ''InterruptedException'' que debo controlar:
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");
// Controlar la excepción
Thread.sleep(500);
}
}
}
----
==== Método join() ====
El método ''join()'' hace que el hilo **desde** el que se invoca al método espera a la finalización del hilo **sobre** el que se invoca. Propaga una excepción de clase ''InterruptedException'' que debo controlar.
En este ejemplo, el hilo principal inicia dos hilos e invoca al método **join()** sobre los dos: espera a que ambos hilos se hayan ejecutado para continuar (o para lo que sea). Los hilos 1 y 2 se ejecutan simultáneamente.
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 del hilo principal y de los dos hilos");
}
En el siguiente caso, el hilo principal (hilo main) espera a que termine el hilo1 para iniciar el hilo2, y luego espera a que hilo2 termine: los hilos 1 y 2 se ejecutan uno después de otro.
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 del hilo principal y de los dos hilos");
}
{{ vimeo>775547420?medium }}
> Sincronización de hilos desde el hilo //main//
{{ vimeo>775547200?medium }}
> Sincronización interna entre hilos, sin depender del hilo //main//
----
{{ ejercicio.png?75}}
=== Ejercicios ===
- ¿Qué pasa si eliminamos las llamadas al método join() en ambos casos?
- Prueba a hacerlo con más hilos. ¿Y con n hilos?
----
==== Método isAlive() ====
''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). Nos permite mantener la ejecución de un hilo mientras otro siga en ejecución:
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?");
}
}
{{ vimeo>775547373?medium }}
> Mantener la ejecución de un hilo dependiendo del estado de otro hilo
----
{{ ejercicio.png?75}}
=== Ejercicios ===
- ¿Podemos asegurar el resultado de la aplicación anterior?
- ¿Y si lo hacemos extendiendo de la clase Thread a la hora de implementar los hilos?
----
==== Detener hilos ====
Dado que ciertos métodos de la clase ''Thread'' para alterar la ejecución de un hilo [[https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html|están obsoletos por no ser seguros]], Java nos anima a que seamos nosotros quienes implementemos formas **limpias** de detener, o pausar y reanudar nuestros hilos.
La forma más sencilla de controlar la ejecución de un hilo que realiza tareas iterativas es utilizando una variable de control, la cual modifico cuando quiera parar el hilo:
private boolean ejecucion;
. . .
public void parar(){
this.ejecucion = false;
}
@Override
public void run() {
while(ejecucion){
//Realizo las tareas
}
}
// En caso de usar un bucle for
@Override
public void run() {
for(int i = 0; i < cantidad; i++)
//Realizo las tareas
. . .
if(!ejecucion){
break;
}
}
}
También podría **pausar** un hilo de forma similar a la anterior. Hay que tener en cuenta que si paro el hilo también debo parar la pausa.
@Override
public void run() {
while(ejecucion){
//Realizo las tareas
. . .
// Si activo la pausa permanezco en el bucle
while(pausado){
//Evito que la comprobación de pausa acapare la cpu
Thread.sleep(200);
//Compruebo si se detiene el hilo mientras está pausado
if(!ejecucion){
pausado = false;
return;
}
}
}
}
Si las condiciones de parada o de pausa son comunes para varios hilos, debo optar por el uso de una **variable atómica** de control para asegurar un sistema //Thread-safe//.
----
=== Variables atómicas ===
En el //package// ''java.util.concurrent.atomic'' tenemos varias clases que nos permiten crear objetos //Thread-Safe//. Algunas de estas clases son [[https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/AtomicBoolean.html|AtomicBoolean]] o [[https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/AtomicInteger.html|AtomicInteger]]. Usar este tipo de datos permite acceder y modificarlos de por diferentes hilos de forma segura.
Podemos acceder o modificar sus valores de forma segura mediante sus métodos ''set()'' y ''get()''.
AtomicBoolean ejecucion = new AtomicBoolean();
ejecucion.set(true);
while(ejecucion.get()){
. . .
}
----
=== Método interrupt() ===
Si un hilo está en una situación de bloqueo porque ha ejecutado ''wait()'', ''sleep()'', ''join()'', etc, no podrá comprobar el estado de una variable de control ya que está bloqueado. Si el bloqueo dura un largo tiempo, debo utilizar el método interrupt().
El método ''interrupt()'' no detiene un hilo, sino que solicita al hilo que se detenga en el primer momento que pueda. Es el programador quien debe gestionar esa //solicitud//.
''interrupt()'' funciona del siguiente modo:
* Si el hilo sobre el que se ejecuta está en un estado de bloque (wait, join o sleep), rompe ese bloqueo produciendo una excepción ''InterruptedException'' y continuando su ejecución.
* Si el hilo no está bloqueado, la llamada a ''interrupt()'' no afecta a la ejecución pero modifica el estado de una variable de control (//interrupt status//) a **true**.
* Para los casos en los que el hilo no está bloqueado, puedo comprobar el estado de interruption mediante el método ''Thread.interrupted()'':
@Override
public void run() {
for (int i = 0; i < cantidad; i++) {
//Operaciones
. . .
if (Thread.interrupted()) {
// Se ha interrumpido, terminamos ejecución
return;
}
}
}
Utilizando las dos formas vistas anteriormente para parar la ejecución de un hilo podemos plantear una solución del siguiente modo:
public class HiloTerminable implements Runnable {
private Thread hilo;
private AtomicBoolean ejecucion;
public HiloTerminable () {
ejecucion = new AtomicBoolean(false);
}
public void start() {
hilo = new Thread(this);
hilo.start();
}
public void stop() {
ejecucion.set(false);
hilo.interrupt();
}
public void run() {
ejecucion.set(true);
while (ejecucion.get()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("El hilo fue terminado por el usuario");
}
// Operaciones del hilo
. . .
}
ejecucion.set(false);
}
}
==== Bloques y métodos Synchronized ====
Un mecanismo de //exclusión mutua//, //mutex// o //monitor// evita que una conjunto de operaciones puedan ser ejecutadas por más de un hilo al mismo tiempo. Protege el acceso a una sección de código (sección crítica) de un acceso concurrente de diversos hilos. La palabra ''synchronized'' nos permite crear un monitor:
* Sólo permitirá ejecutar un bloque o método //synchronized// por un hilo a la vez. Por tanto, si existen varios bloques/métodos //synchronized// dentro de un objeto, sólo uno de ellos podrá ejecutarse al mismo tiempo por diversos hilos.
* El resto de hilos que también necesitan acceder a los bloques/metodos //synchronized// permanecerán bloqueados.
* Cuando el hilo finaliza la ejecución de un bloque/método //synchronized// todos los hilos que estaban en espera para obtenerlo, se reactivarán y un hilo al azar accederá a la sección.
public class Sincrona{
private Object monitor;
. . .
//Solo un hilo puede ejecutar el siguiente método al mismo tiempo
//El resto de hilos permanecen bloqueados hasta que termina su ejecución
public synchronized void accesoExclusivo(){
// Operaciones
}
. . .
//Lo mismo ocurre con este bloque
synchronized(monitor){
// Operaciones
}
}
----
{{ ejercicio.png?75}}
=== Ejercicios ===
- ¿Teniendo en cuenta el concepto de exclusión mutua que aplica //synchronized//, cómo podríamos contar del 1 al 10 entre dos hilos, de modo que cada hilo cuenta una unidad alternativamente?
----
===Métodos wait() y notify()===
Al utilizar un monitor (//synchronized//), Java nos permite utilizar algunos métodos pertenecientes a la clase [[https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html|Object]] para sincronizar hilos:
* ''wait()'': El hilo que invoca al método para su ejecución hasta que otro hilo llame a ''notify'' o ''notifyAll''.
* ''notify()'': Continua la ejecución del hilo que invocó ''wait()'' y desbloquea un hilo al azar para acceder al monitor.
* ''notifyAll()'': Continua la ejecución del hilo que invocó ''wait()'' y desbloquea todos los hilos que quieren acceder al monitor.
private boolean pausado;
. . .
@Override
public void run() {
for(int i = 0; i < cantidad; i++){
if(pausado){
esperar();
}
//operaciones
}
}
public synchronized void esperar(){
try {
wait();
} catch (InterruptedException e) {
System.out.println("Hilo parado por el usuario");
}
}
public void pausar(){
pausado = true;
}
public synchronized void continuar(){
pausado = false;
notifyAll();
}
. . .
public static void main(String[] args){
Thread hiloPausado = new Thread();
hiloPausado.start();
//Suspendo su ejecución
hiloPausado.pausar();
//Reanudo la ejecución
hiloPausado.continuar();
}
En el ejemplo anterior vemos que podemos utilizar estos métodos para detener y reanudar la ejecución de un hilo.
----
====Colecciones Thread-safe====
La clase ''Collections'' nos permite crear estructuras de datos seguras ante lecturas y modificaciones concurrentes a partir de una estructura de datos cualquiera:
ArrayList lista = new ArrayList<>();
List listaSincronizada = Collections.synchronizedList(lista);
//Ahora los accesos y modificaciones son thread-safe
listaSincronizada.add(persona1);
listaSincronizada.get(2);
listaSincronizada.remove(persona1);
Sin embargo, si lo que queremos es recorrer la colección, es el programador el que debe asegurar un acceso seguro a dicha iteracion:
//Indicamos ''synchronized'' para asegurar el acceso de un solo hilo
public synchronized void recorrerLista(){
for(Persona persona : listaSincronizada){
persona.hacerOperacion();
}
}
===== Uso de hilos en GUIs Swing =====
Como se detalla en la documentación de Java, [[https://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html|Concurrencia en Swing]], una aplicación con GUI es ejecutada por Java usando diferentes hilos, concretamente 3 tipos de hilos:
* Un hilo inicial, o hilo Main: arranca la ventana y termina.
* El hilo //Event-Dispatch Thread// (EDT): se inicia cuando se hace visible la ventana.
* Algunos //Worker Threads//: hilos en segundo plano con cargas de trabajo pesadas.
Si en una aplicación con GUI voy a realizar tareas de larga carga de proceso debo entender el funcionamiento del hilo EDT:
==== Event-Dispatch Thread (EDT) e hilos Worker====
Todo el código de manejo de eventos, de visualización, y de actualización de componentes gráficos es ejecutado por Java en el //Event-Dispatch Thread//, el cual está diseñado para realizar operaciones cortas en respuesta a eventos.
Al ejecutar todos los manejos de eventos en el mismo hilo, Java se asegura que el bloque de operaciones en respuesta a un evento se ejecuta completamente antes de atender al siguiente evento. Si al responder a un evento, nuestro programa realiza tareas de larga ejecución, **la ventana se congelará y no responderá** hasta que el //EDT// termine de realizar dicha operación.
Para diseñar aplicaciones con GUIs Swing que sean //Thread-Safe// debo asegurar que toda intrucción que acceda o modifique el estado de la interfaz gráfica se ejecute en el hilo //EDT//((Documentación oficial https://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html)). Mientras que el acceso o modificación de componentes Swing se produzcan en los bloques de los //listeners//, no tendremos problema.
Podemos comprobar si un bloque de código se está ejecutando en el //EDT//, llamando al método ''isEventDispatchThread()'':
// Muestro la respuesta por consola a modo de comprobación
System.out.println(SwingUtilities.isEventDispatchThread());
En resumen, **nuestras aplicaciones con GUI deben respetar estas dos premisas:**(( Indicado, tambien, en la documentación de la clase SwingWorker https://docs.oracle.com/javase/8/docs/api/javax/swing/SwingWorker.html))
- Las operaciones con alta carga de ejecución no deben ser ejecutadas en el hilo EDT, para no bloquear la GUI.
- Las operaciones que accedan o modifiquen componentes gráficos de //swing//, siempre deben ser ejecutadas en el EDT.
Por lo tanto, como programador:
* Debo mantener las operaciones de un //listener// lo más sencillas posibles, creando un hilo (//worker thread//) para ejecutar cualquier tarea lenta (tareas lentas, accesos a ficheros, sockets, conexiones a bbdd, etc).
* Si desde un hilo //worker// con una tarea de larga duración, necesito modificar el estado de componentes de Swing, debo enviar esas tareas de modificación a la cola del EDT.
El problema puede surgir **cuando realizamos modificaciones sobre la GUI desde hilos creados por nosotros**, ya que estas operaciones dejarían de ejecutarse en el EDT. Para controlar que las operaciones sobre la GUI se ejecutan en el hilo destinado a tal fin (EDT) podemos usar:
* método [[https://docs.oracle.com/javase/8/docs/api/javax/swing/SwingUtilities.html#invokeLater-java.lang.Runnable-|SwingUtilities.invokeLater()]]
* clase [[https://docs.oracle.com/javase/8/docs/api/javax/swing/SwingWorker.html|SwingWorker]]
==== Método invokeLater() ====
Si en mi aplicación he implementado un hilo para realizar una tarea pesada y debo realizar operaciones que acceden o actualizan el estado de algún componente de la GUI, puedo añadir esa pequeña operación de acceso a la GUI a la cola de eventos para asegurarme que esas instrucciones se ejecuten en el //EDT//:
private JProgressBar pbProgreso;
private JLabel lblContador;
. . .
// Método que responde a un evento sobre un botón
@Override
public void actionPerformed(ActionEvent e) {
//Tarea en segundo plano de larga duración
Thread hilo = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 1; i < tiempo; i++){
Thread.sleep(500);
//Aseguro que la modificación de la GUI se hace en EDT
actualizarGUI(i, tiempo);
}
}
});
}
. . .
private void actualizarGUI(int cuenta, int total){
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
lblContador.setText("Contador: " + cuenta);
pbProgreso.setValue(cuenta * 100 / total);
}
});
}
El método ''actualizarGUI()'' realiza lo siguiente:
- llama a ''invokeLater()''
- el método ''invokeLAter()'' crea una tarea //Runnable// y la añade a la cola del hilo EDT
- el método invokeLater() termina su ejecución
- el hilo EDT ejecutará dicha tarea (modificar los dos componentes Swing) cuando le llegue su turno
Si programamos adecuadamente una GUI, las tareas del EDT serás cortas y rápidas, y la respuesta será inmediata.
==== Clase SwingWorker ====
En algunas situaciones puede que el método ''invokeLater()'' no sea tan cómodo de utilizar, como cuando realizamos operaciones de larga duración. La clase ''SwingWorker'' crea un hilo de ejecución, que nos permite acceder de forma segura a la GUI.
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//, realizando las operaciones de acceso a la GUI en el //Event Dispatch Thread//.
La clase ''SwingWorker'' tiene los siguientes métodos:
* ''doInBackground()'': Método que realiza la tarea pesada del hilo. Permite llamar al método publish(). Es el único método obligatorio, ya que es abstracto.
* ''[[https://docs.oracle.com/javase/8/docs/api/javax/swing/SwingWorker.html#done--|done()]]'': método que se ejecuta en el **EDT**, al terminar la ejecución de doInBackGround().
Si queremos además contemplar el progreso de la tarea tenemos:
* ''publish()'': para enviar valores desde doInBackground(), con los que actualizar componentes de swing.
* ''[[https://docs.oracle.com/javase/8/docs/api/javax/swing/SwingWorker.html#process-java.util.List-|process()]]'': método que se ejecuta en el **EDT**, recibe una lista con los valores enviados por publish(). Se ejecuta regularmente.
Visto esto, **podemos actualizar componentes GUI al terminar la tarea (done()) y durante el progreso de la misma (process())**.
Además el método doInBackground() permite devolver un objeto, por ejemplo el resultado de una carga de ficheros, o de una transferencia.
public class Tarea extends SwingWorker {
private JProgressBar barraProgreso;
private JLabel labelEstado;
// Se pasa como parametro componentes swing
public Tarea(JProgressBar barraProgreso, JLabel labelEstado) {
this.barraProgreso = barraProgreso;
this.labelEstado = labelEstado;
}
@Override
protected Void doInBackground() throws Exception {
// realiza una tarea pesada mediante un bucle (p.e. copiar un fichero)
while(...){
. . .
// Se notifica el avance (valor entre 0 y 100)
publish(avanceCarga);
}
return null;
}
@Override
protected void process(List valores) {
// En la lista valores obtengo lo que envía publish()
// Se actualiza en el EDT de forma automática
barraProgreso.setValue(valores.get(0));
}
@Override
protected void done() {
// Se ejecuta en el EDT al terminar doInBackground()
labelEstado.setText("Fichero copiado");
}
}
public class ProgramaGUI {
. . .
// Lanza la ejecucion de la tarea en segundo plano
// También le paso los 2 componentes GUI que quiero actualizar en el EDT
Tarea tarea = new Tarea(barra, label);
tarea.execute();
}
=== Parar hilo Worker ===
En caso de querer finalizar las tareas del hilo worker por nuestra cuenta, podemos hacer uso del método ''cancel(true)'', el cual modifica el estado de ejecución de la tarea. Esto no detiene el hilo Worker directamente, sino que modificará un //flag// de estado que tiene la clase //SwingWorker//. Para comprobar el estado desde nuestra clase Worker, usaremos el método ''isCancelled()''.
. . .
@Override
protected Void doInBackground() throws Exception {
// además de controlar el progreso, comprobamos se se ha llamado a cancel()
while(... && !isCancelled()){
. . .
//Realizo las tareas
}
return null;
}
. . .
public static void main(String[] args){
TareaWorker tarea = new TareaWorker();
tarea.execute();
. . .
tarea.cancel(true);
}
Debemos de tener en cuenta que al cancelar la iteración de un bucle, el método doInBackground() terminará su ejecución. Y después se ejecutará también el método ''done()'', independientemente de si se ha llamado a ''cancel()''.
{{ vimeo>782181802?medium }}
> Clase SwingWorker
=== Property Change Listener ===
Es un manejador de eventos que puedo vincular a cualquier tipo de objeto del que quiera ser notificado ante algún cambio en su estado. En este caso voy a ver cómo puedo hacer que mi programa principal se entere de los cambios producidos en mi objeto SwingWorker, para poder actualizar la GUI desde un manejador de eventos que se ejecuta en el EDT.
La clase SwingWorker tiene dos propiedades((Se explica en la documentación oficial https://docs.oracle.com/javase/tutorial/uiswing/concurrency/bound.html)), cuyos cambios serán notificados al PropertyChangeListener:
* La propiedad ''progress'': se modifica mediante el método ''setProgress()'' de la clase SwingWorker y admite valores entre 0-100.
* La propiedad ''state'': indica el estado del objeto SwingWorker:
* //STARTED//: Es el estado del SwingWorker cuando ejecuta ''doInBackground()''
* //PENDING//: Es el estado previo al inicio de ''doInBackground()''
* //DONE//: Es el estado que tiene cuando ha terminado ''doInBackground()''.
Además, SwingWorker nos permite generar eventos ''PropertyChangeEvent'' en cualquier otro momento usando el método:
''firePropertyChange()''
Notificar cambios en un objeto:
public class Tarea extends SwingWorker {
@Override
public Void doInBackground() throws Exception {
while(condicion){
// Tarea pesada
. . .
// Se notifica el avance modificando la propiedad "progress"
setProgress(avanceCarga);
}
//Si la tarea se cancela sin llegar a terminar, creo un evento
if(!isCancelled()){
firePropertyChange("cancelada", 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("cancelada")) {
//Tarea cancelada: notificarlo en la GUI
}
}
});
tarea.execute();
}
{{ vimeo>782181847?medium }}
> Actualizar GUI con PropertyChangeListener
----
{{ ejercicio.png?75}}
=== Ejercicios ===
- 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
- ¿Cómo harías la aplicación anterior para que se puedan fijar varias Alarmas simultáneas a distintas horas?
- ¿Y si necesitas que una aplicación compruebe algo a intervalos regulares de tiempo? Por ejemplo, si hay actualizaciones, auto-guardar un documento, . . .
----
===== Ficheros de Registro (Logs) =====
A continuación se muestra, utilizando la librería [[http://logging.apache.org/log4j/2.x/|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
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 =====
{{ ejercicio.png?75}}
- 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\\ \\
- 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\\ \\
- 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\\ \\
- 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)\\ \\
- 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.{{ cronometro.png }}\\
----
===== Proyectos de ejemplo ======
Todos los proyectos realizados en los videos y otros de ejemplo de este tema se pueden encontrar en el [[ https://bitbucket.org/fvaldeon/psp-hilos|repositorio de hilos]] de Bibucket.
Los proyectos de los ejercicios que se vayan haciendo en clase estarán disponibles en el [[https://bitbucket.org/fvaldeon/psp-ejercicios22-23|repositorio psp-ejercicios de BitBucket]]
----
===== Prácticas =====
* **Práctica 1.1** Creación de una aplicación multihilo
----
(c) {{date> %Y}} Santiago Faci y Fernando Valdeón