Programación

1º DAM/DAW - Curso 2024-2025

User Tools

Site Tools


apuntes:concurrencia

Differences

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

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
apuntes:concurrencia [2023/05/28 11:43] – [Streams paralelos] Santiago Faciapuntes:concurrencia [2023/05/28 23:59] (current) Santiago Faci
Line 63: Line 63:
 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: 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:
  
-  * 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. +  * 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. 
-  * 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.+  * 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.
   * 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.   * 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.
  
Line 146: Line 146:
 En cualquier caso tenemos que tener siempre en cuenta las siguientes consideraciones: En cualquier caso tenemos que tener siempre en cuenta las siguientes consideraciones:
  
-  * Siempre se debe sobreescribir (Override) el método run() e implementar allí lo que tiene que hacer el hilo+  * Siempre se debe sobreescribir (Override) el método ''run()'' e implementar allí lo que tiene que hacer el hilo
   * 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?)   * 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?)
   * 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   * 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
Line 152: Line 152:
   * Además, en el caso de aplicaciones multihilo, también nos puede interesar sincronizar y comunicar unos hilos con otros   * Además, en el caso de aplicaciones multihilo, también nos puede interesar sincronizar y comunicar unos hilos con otros
  
-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.+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.
  
 ==== Sincronización de hilos ==== ==== 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:+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 continuar +  - ''join()'' Se espera la terminación del hilo que invoca a este método antes de continuar 
-  - //Thread.sleep(int)//  El hilo que ejecuta esta llamada permanece dormido durante el tiempo especificado como parámetro (en ms) +  - ''Thread.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) +  - ''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)+  - ''yield()'' Sugiere al scheduler que sea otro hilo el que se ejecute (no se asegura)
  
 <code java> <code java>
Line 276: Line 276:
 </code> </code>
  
-//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)+''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 ===== ===== Programación concurrente =====
Line 283: Line 283:
  
   * En un entorno multi-hilo, se da una condición de carrera cuando más de un hilo intenta "al mismo tiempo" actualizar un recurso compartido (que puede ser una variable)   * En un entorno multi-hilo, se da una condición de carrera cuando más de un hilo intenta "al mismo tiempo" actualizar un recurso compartido (que puede ser una variable)
-  * Java proporciona un mecanismo para evitar estos problemas (palabra reserva //synchronized//)+  * Java proporciona un mecanismo para evitar estos problemas (palabra reserva ''synchronized'')
   * Evita que más de un hilo puedan acceder a un zona de código o ejecutar un método determinado   * Evita que más de un hilo puedan acceder a un zona de código o ejecutar un método determinado
  
Line 296: Line 296:
 </code> </code>
  
-Sin la palabra reservada //synchronized//, si más de un hilo ejecuta el método calcular, los resultados obtenidos pueden ser bastante inesperados (hacer prueba con y sin ella)+Sin la palabra reservada ''synchronized'', si más de un hilo ejecuta el método calcular, los resultados obtenidos pueden ser bastante inesperados (hacer prueba con y sin ella) 
 ==== Executors y pools de hilos ==== ==== Executors y pools de hilos ====
  
-En el apartado anterior vimos como crear y lanzar hilos individualmente utilizando la clase //Thread// que Java proporcina en su API. El +En el apartado anterior vimos como crear y lanzar hilos individualmente utilizando la clase ''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, ''Executors'', que permite una gestión mucho más sencillo de
-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, //Executors//, que permite una gestión mucho más sencillo de+
 cualquier tarea que se ejecuta en segundo plano. cualquier tarea que se ejecuta en segundo plano.
  
-Utilizando //Executors//, como programador, te olvidas en parte de la gestión de los hilos. Simplemente hay que centrarse en programar +Utilizando ''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 ''Runnable'' ) a un ''Executor'' , que se encargará de crear el hilo, lanzarlo y gestionar su ejecución.
-el código a realizar y asignar dicho código (una clase //Runnable// ) a un //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 +Veamos un ejemplo de cómo lanzar una serie de tareas en segundo plano utilizando la clase ''ExecutorService''  que viene con el framework ''Executors'' de Java.
-//Executors// de Java.+
  
-Supongamos una clase muy sencilla que implementa \verb Runnable  para definir un código que debe ejecutarse en segundo plano:+Supongamos una clase muy sencilla que implementa ''Runnable'' para definir un código que debe ejecutarse en segundo plano:
  
 <code java> <code java>
Line 323: Line 319:
 </code> </code>
  
-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  +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 ''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.
-//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 //shutdown()//, que hará finalizar el //Executor// una vez hayan terminado todas las tareas que se +Finalmente se ejecuta el método ''shutdown()'', que hará finalizar el ''Executor'' una vez hayan terminado todas las tareas que se encuentran ejecutándose con él.
-encuentran ejecutándose con él.+
  
-Hay que tener en cuenta que existe también un método //shutdownNow//  que hace que el //Executor// finalice devolviendo todas las +Hay que tener en cuenta que existe también un método ''shutdownNow'' que hace que el ''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.
-tareas que estaban esperando y no llegaron a ejecutarse. Se esperará que las tareas activas terminen o bien sean interrumpidas.+
  
 <code java> <code java>
Line 349: Line 340:
 </code> </code>
  
-También es posible trabajar con un //ExecutorService// que simplemente cuente con un hilo+También es posible trabajar con un ''ExecutorService'' que simplemente cuente con un hilo
  
 <code java> <code java>
Line 379: Line 370:
 En cualquier caso, podemos encontrarnos con dos tipos de pools de hilos: En cualquier caso, podemos encontrarnos con dos tipos de pools de hilos:
  
-  * **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. +  * **Fixed**: Se pueden crear a través de los métodos ''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. 
-  * **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.+  * **Cached**: Se pueden crear a través de los métodos ''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.
  
 ==== Callable y Future ==== ==== Callable y Future ====
  
-Hasta el momento hemos trabajado con objetos //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 //run()// no devuelve nada). Tampoco pueden lanzar excepciones puesto que el método //run()// de //Runnable// no está prearado para ello. +Hasta el momento hemos trabajado con objetos ''Runnable'' como objetos que se pueden asignar a un hilo para ejecutar su código en segundo plano. Los objetos ''Runnable'' no son capaces de devolver ningún valor, simplemente ejecutan el código (el método ''run()'' no devuelve nada). Tampoco pueden lanzar excepciones puesto que el método ''run()'' de ''Runnable'' no está prearado para ello. 
-Java proporciona también una interface llamada //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.+Java proporciona también una interface llamada ''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.
  
 <code java> <code java>
Line 397: Line 388:
 </code> </code>
  
-Ligado a este nuevo tipo de tarea //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.+Ligado a este nuevo tipo de tarea ''Callable'' aparece un nuevo tipo llamado ''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 ''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 //Callable// y cómo se recoge su resultado:+Veamos ahora un ejemplo lanzando una tarea ''Callable'' y cómo se recoge su resultado:
  
 <code java> <code java>
Line 422: Line 413:
 </code> </code>
  
-Hay que tener en cuenta que la llamada al método //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 //get()//+Hay que tener en cuenta que la llamada al método ''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 ''Future'' y podemos seguir ejecutando instrucciones hasta que realmente necesitemos utilizar el valor, que será cuando invoquemos al método ''get()''. 
-Además, siempre podremos cancelar el objeto //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 //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.+ 
 +Además, siempre podremos cancelar el objeto ''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 ''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.
  
 <code java> <code java>
Line 487: Line 479:
 </code> </code>
  
-En cualquier caso, el método //get()// puede ser invocado para establecer un tiempo de //timeout//, a partir del cual el método devolvería una excepción //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.+En cualquier caso, el método ''get()'' puede ser invocado para establecer un tiempo de //timeout//, a partir del cual el método devolvería una excepción ''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.
  
 <code java> <code java>
Line 496: Line 488:
 </code> </code>
  
-Además, es posible lanzar varias tareas al mismo tiempo con el método //executeAll// de la clase //ExecutorService//:+Además, es posible lanzar varias tareas al mismo tiempo con el método ''executeAll'' de la clase ''ExecutorService'':
  
 <code java> <code java>
Line 511: Line 503:
 ==== CompletableFuture ==== ==== CompletableFuture ====
  
-Proporciona una API más completa que la que hay para la clase //Future//.+Proporciona una API más completa que la que hay para la clase ''Future''.
  
   * Lanzar una tarea en segundo plano de forma asíncrona y ejecutar un método al finalizar ésta (a través de la llamada a un método callback)   * Lanzar una tarea en segundo plano de forma asíncrona y ejecutar un método al finalizar ésta (a través de la llamada a un método callback)
Line 566: Line 558:
 ==== CyclicBarrier ==== ==== CyclicBarrier ====
  
-Similar a CountDownLatch pero reutilizable+Similar a ''CountDownLatch'' pero reutilizable
  
 Se utiliza para que los hilos se esperen los unos a los otros antes de realizar alguna tarea concreta Se utiliza para que los hilos se esperen los unos a los otros antes de realizar alguna tarea concreta
Line 595: Line 587:
 } }
 </code> </code>
 +
 ==== Colecciones sincronizadas ==== ==== Colecciones sincronizadas ====
  
-Son lo que se conocen como colecciones thread-safe, puesto que están listas para usarse en entornos de concurrencia con varios hilos ejecutándose.+Son lo que se conocen como colecciones //thread-safe//, puesto que están listas para usarse en entornos de concurrencia con varios hilos ejecutándose. 
 + 
 +En la clase ''Collection'' hay una serie de métodos estáticos que devuelven instancia de colecciones sincronizadas. Por ejemplo:
  
-En la clase Collection hay una serie de métodos estáticos que devuelven instancia de colecciones sincronizadas. Por ejemplo: 
   * synchronizedList()   * synchronizedList()
   * synchronizedMap()   * synchronizedMap()
   * synchronizedSet()   * synchronizedSet()
  
-Además, existe alguna colección ya sincronizada como //Vector// (similar a //ArrayList//) //HashTable// (similar a //HashMap//)+Además, existe alguna colección ya sincronizada como ''Vector'' (similar a ''ArrayList'' ''HashTable'' (similar a ''HashMap'')
  
 ==== Objetos atómicos ==== ==== Objetos atómicos ====
Line 610: Line 604:
 Los objetos atómicos son estructuras Java que sirven para representar valores y realizar operaciones de forma atómica para algunos tipos de datos. Los objetos atómicos son estructuras Java que sirven para representar valores y realizar operaciones de forma atómica para algunos tipos de datos.
  
-//AtomicInt////AtomicLong////AtomicBoolean// son algunos de los objetos atómicos que tenemos disponibles dentro de la API de Java+''AtomicInt''''AtomicLong//''AtomicBoolean'' son algunos de los objetos atómicos que tenemos disponibles dentro de la API de Java
  
-Por poner algún ejemplo, //AtomicInt// garantiza la ejecución atómica de algunas operaciones como estas: +Por poner algún ejemplo, ''AtomicInt'' garantiza la ejecución atómica de algunas operaciones como estas: 
-  * get() +  * ''get() 
-  * compareAndSet(valorComparacion, nuevoValor) +  * ''compareAndSet(valorComparacion, nuevoValor)'' 
-  * addAndGet(valor) +  * ''addAndGet(valor)'' 
-  * getAndIncrement() +  * ''getAndIncrement()'' 
-  * incrementAndGet() +  * ''incrementAndGet()'' 
-  * decrementAndGet() +  * ''decrementAndGet()'' 
-  * getAndDecrement()+  * ''getAndDecrement()''
  
  
Line 644: Line 638:
  
 <figure> <figure>
-{{ parallel_sequential.png }}+{{ parallel-sequential.png }}
 <caption>Ejecución stream secuencial vs stream paralelo</caption></figure> <caption>Ejecución stream secuencial vs stream paralelo</caption></figure>
  
Line 659: Line 653:
  
 <code java> <code java>
-  long count = Stream.iterate(0, i -> i + 1) +long count = Stream.iterate(0, i -> i + 1) 
-    .limit(500000) +  .limit(500000) 
-    .parallel()  +  .parallel()  
-    .filter(Main::isPrime)  +  .filter(Main::isPrime)  
-    .count();+  .count();
 System.out.println("Hay " + count + " números primos"); System.out.println("Hay " + count + " números primos");
 </code> </code>
Line 669: Line 663:
 ==== Locks ==== ==== Locks ====
  
-Es otro mecanismo para la sincronización de hilos parecido a como hace //synchronized// +Es otro mecanismo para la sincronización de hilos parecido a como hace ''synchronized'' 
-Es algo más flexible que synchronized, puesto que podemos bloquear/desbloquear la zona exclusiva en diferentes métodos + 
-//Lock// es un interface que tiene varias opciones para implementar. Veremos un ejemplo con //ReentrantLock//+Es algo más flexible que ''synchronized'', puesto que podemos bloquear/desbloquear la zona exclusiva en diferentes métodos  
 + 
 +''Lock'' es un interface que tiene varias opciones para implementar. Veremos un ejemplo con ''ReentrantLock''
  
 <code java> <code java>
Line 683: Line 679:
 </code> </code>
  
-Podemos encapsular la operación de bloqueo (lock) para evitar que se produzcan deadlocks (esperar indefinidamente un recurso) utilizando el método //tryLock// indicando el tiempo que el hilo esperará por el recurso+Podemos encapsular la operación de bloqueo (lock) para evitar que se produzcan deadlocks (esperar indefinidamente un recurso) utilizando el método ''tryLock'' indicando el tiempo que el hilo esperará por el recurso
  
 <code java> <code java>
Line 699: Line 695:
 Patrón de diseño por el cual un objeto, llamada sujeto, mantiene a otros objetos, llamados observadores, notificados acerca de cualquier cambio que sobre él se produce. Patrón de diseño por el cual un objeto, llamada sujeto, mantiene a otros objetos, llamados observadores, notificados acerca de cualquier cambio que sobre él se produce.
  
-Hasta Java 9, se proporcionaba una serie de clases que permitían definir una solución para este Patrón. A partir de entonces se recomienda el uso de //PropertyChangeListener// para dicha implementación.+Hasta Java 9, se proporcionaba una serie de clases que permitían definir una solución para este Patrón. A partir de entonces se recomienda el uso de ''PropertyChangeListener'' para dicha implementación.
  
 === Clase Observable === === Clase Observable ===
apuntes/concurrencia.1685274187.txt.gz · Last modified: 2023/05/28 11:43 by Santiago Faci