Download Notas de clase - Nachintoch Desarrollos

Document related concepts
no text concepts found
Transcript
Licenciatura en Ciencias de la Computación, Facultad de Ciencias, UNAM.
Computación concurrente.
Profesor: Carlos Zerón Martínez.
Ayudante: Manuel Ignacio Castillo López.
Notas de clase
Threads
Introducción
Ya hemos visto que al desarrollar aplicaciones gráficas en Java, el comportamiento de la
misma no está regido estrictamente por el método main; sino que usualmente la aplicación
espera a que el usuario haga alguna petición. Pero ¿cómo funciona esta espera? ¿este
comportamiento es propio de las interfaces gráficas?
Lo anterior es posible gracias a los hilos de ejecución. Cualquier tipo de aplicación puede
tener más de un hilo y todos influyen en el comportamiento de la misma. Las aplicaciones
gráficas en particular, tienen más de un hilo para: atender al usuario (los eventos, en niveles
más bajos; son interrupciones y un hilo o más responden a estas), para dibujar la pantalla
(de lo contrario la aplicación pasaría la mayor parte de su tiempo irresponsiva, mientras
refresca la memoria de vídeo); entre otras.
En cualquier plataforma, todo proceso tiene por lo menos un hilo. En el caso particular de la
programación en Java; al invocar un programa, se le asigna un hilo principal donde se
ejecuta su método main: cada programa corre en su propio hilo. A partir de main, la
aplicación puede crear más hilos: tantos como los necesite. Este comportamiento es muy
similar en muchas otras plataformas de desarrollo de software.
Es importante mencionar que estos nuevos hilos son concurrentes, ya que se ejecutan al
mismo tiempo en la misma máquina. Además, el programa no terminará de ejecutarse sino
hasta que todos sus hilos terminen de hacer también: no importa que el hilo del método
main termine antes que todos los demás; mientras otros hilos continúen ejecutándose, la
aplicación también lo hará.
Ejercicio. Revise el ejemplo “Interfaz no responsiva”. Observe con detalle los métodos que
gestionan los eventos de los botones y ejecute el programa. ¿por qué la aplicación se
congela hasta terminar de contar?
Hilos en Java
Si queremos crear un programa en Java que realice más de una tarea al mismo tiempo;
podríamos tomar la estrategia clásica de ejecutar dos programas diferentes en un sistema
multitarea; ya sea usando dos terminales distintas o solicitando que se envíe cada instancia
del programa a segundo plano. Esta es la estrategia de crear varios procesos, pero tiene
varios inconvenientes: la administración de los recursos compartidos es más engorrosa de
lo que debería ser y los procesos tienen un contexto relativamente grande; formado por su
propio heap, su propio stack, sus propias variables, etc, lo que hace que su construcción y
administración sea también relativamente lenta.
La otra estrategia; y la más apropiada para muchos de los problemas, es usar hilos: un solo
proceso que realiza varias tareas a la vez (como hemos expuesto anteriormente). Además;
los hilos al ser parte de un proceso, no requieren de una inicialización tan elaborada y su
ejecución y mantenimiento es más eficiente.
Volviendo a Java; puesto que es un lenguaje orientado a objetos, no debería sorprendernos
que los hilos los modele como objetos. Se representan con la clase java.lang.Thread y
como puede suponer; al ser parte de java.lang, están presente en todos los programas de
Java (recordemos que todo proceso -programa- debe tener al menos un hilo). Thread nos
permite acceder al hilo sobre el que se estemos ejecutando, crear hilos nuevos, mantener
un grupo de ellos (conocidos como thread pools), terminar un hilo; entre otras.
Thread es una clase concreta, y por lo mismo ya tiene definidos todos sus métodos y
variables con los que trabaja… Entonces; ¿como es que podemos usar hilos para hacer lo
que nosotros queramos, si ya están definidos? Afortunadamente y a diferencia de
java.lang.Integer o java.lang.String, Thread no es una clase final y podemos extenderla
(heredar de ella). Además, los hilos en Java son definidos por la interfaz java.lang.Runnable
(Thread implementa esta interfaz) que también podemos usar para crear nuestras
operaciones concurrentes.
Runnable define un único método llamado run; no devuelve ni recibe nada y su firma es la
siguiente:
void run();
Las instrucciones de run es lo que Java ejecuta concurrentemente en nuestros hilos. Así;
tenemos dos formas (principales) de crear hilos:
● Extendiendo a Thread y sobreescribiendo el método run en nuestra nueva clase.
● Implementando a Runnable y pasar nuestra clase a un Thread para que ejecute su
método run (a continuación veremos esto con más detalle).
Es importante mencionar que fuera de pequeños experimentos, es recomendado no
instanciar hilos mediante el constructor de Thread, ya que Java nos proporciona alternativas
mucho más eficientes; como veremos más adelante. Con lo anterior establecido, veamos
algunas de las operaciones más utilizadas con hilos en Java.
Constructor
Thread define un constructor por omisión; que es suficiente para pequeños ejemplos y
experimentos, pero no recomendado para programas más serios. El hilo construido con
cualquiera de los constructores, estará listo para ejecutar su método run de forma
concurrente. Podemos pasarle al constructor una cadena para que el hilo la use como su
nombre. Este es arbitrario y nos debería ayudar a identificarlo de los demás hilos en
ejecución en nuestra aplicación (es decir, deberíamos evitar nombrar a nuestros hilos
patito1… A menos que el hilo represente a un pato de entre un grupo de patos).
Otro constructor, toma como argumento una instancia de Runnable. Este hilo en lugar de
ejecutar su propio método run, ejecutará el del Runnable dado. También podemos pasarle la
cadena adicional para nombrar el hilo. Además, podemos indicar al constructor una
referencia al grupo de hilos al que pertenecerá el mismo.
Los grupos de hilos son un objeto modelado por la clase java.lang.ThreadGroup y es
importante no confundirlos con los thread pools; modelados por otra clase que veremos más
adelante. ThreadGroup nos permite agrupar varios hilos y obtener información general
sobre todos ellos. La idea es que los hilos agrupados realicen algún tipo de tarea en común.
Esto es diferente a la filosofía de los thread pools, que preparan recursos para la
inicialización y ejecución de hilos de forma más eficiente.
Demonios
El primer método; además de los constructores, que señalaremos de la clase Thread, es el
setter void setDaemon(boolean). Con esta instrucción podemos especificarle a Java que
el hilo en cuestión deberá ser tratado como un demonio (al pasarle un true. Por omisión es
falso). Los demonios son hilos que por alguna razón queremos que continúen ejecutándose
aún cuando la aplicación termina: termina el proceso de la JVM junto con nuestra
aplicación, pero los demonios pueden seguirse ejecutando.
Normalmente los demonios ofrecen algún tipo de servicio. Por ejemplo, si nuestra aplicación
es un reloj temporizador de segundo plano, podemos usar un demonio que duerma tanto
tiempo como el tiempo que se quiere esperar hasta ejecutar algún evento. Podemos tener
demonios que esperan por algún mensaje de red para implementar mensajeros
instantáneos, etc.
Prioridades
Además de setDaemon; Thread define otro setter que puede ser de gran utilidad en algunas
aplicaciones, este es void setPriority(int). Los hilos en java son de usuario: la
administración de su ejecución es realizada por la máquina virtual. La JVM usa prioridades
para calendarizar los hilos; cuyo rango va de Thread.MIN_PRIORITY a
Thread.MAX_PRIORITY. Entre más cercano sea el valor de la prioridad de un hilo a
MAX_PRIORITY, mayor será la prioridad de ejecución que le otorgará la JVM.
Cambio del estado de un hilo: Sleep, notify y wait
Otra de las instrucciones importantes para usar es static void Thread.sleep(long). Como
su nombre sugiere, pone a dormir un hilo. El parámetro que lleva, es la cantidad de tiempo
en milisegundos que dormirá el hilo. Este lapso de tiempo suele ser bastante preciso, por lo
que podemos asumir los tiempos de sueño para crear esperas entre hilos (no ocurrirá que
los hilos duerman 5 minutos más). Mientras un hilo duerme no ejecuta ninguna instrucción;
está detenido. Al despertarlo, reanuda su ejecución en la instrucción que siga a la
instrucción que lo durmió (sleep).
Podemos despertar al hilo mientras duerme sin importar que todavía le quede tiempo de
sueño. Para ello usamos el método de la clase java.lang.Object void notify(). Object define
algunos métodos que nos permiten detener y reanudar el hilo sobre el que se ejecute el
objeto en el que se haga la llamada.
Con lo anterior dicho, es importante mencionar que el método void Object.wait() es similar
a Thread.sleep(), pero no especifica cuándo debe despertar el hilo; y a menos que se llame
a notify() posteriormente, el hilo nunca despertará.
Es importante considerar que cuando despertamos un hilo con notify; se dispara una
excepción llamada InterruptedException. A lo largo del curso, nos encontraremos con
problemas que involucran esperar a otros hilos. Cuando forzamos la terminación de una
espera de un hilo también dispara una InterruptedException.
Terminación de hilos
Finalmente, tenemos void Thread.join(). Este método termina el hilo y libera los recursos
que estuviera ocupando. Puesto que el hilo podría haber sido interrumpido al solicitar su
destrucción, join puede arrojar una InterruptedException. Una estrategia para terminar hilos
en Java con esto en consideración, es la siguiente:
boolean retry = true;
while(retry) {
try {
thread.join();
retry = false;
} catch(InterruptedException e) {
Logger.getLogger(“Mi-app”)
.log(Level.INFO, “El hilo fue interrumpido”, e);
}
}
A pesar de consistir en un ciclo, esto debería tomar muy poco tiempo; un par de iteraciones.
Si el programa intenta terminar un hilo y no puede hacerlo en menos de 10 iteraciones, lo
más probable es que haya algún problema en el diseño de la concurrencia de la aplicación.
Intentar terminar un hilo que ejecuta un ciclo while cuya condición de permanencia sigue
siendo verdadera hasta este punto, resultará en un fallo: no podemos terminar hilos que no
hayan terminado de ejecutar su método run.
Por lo anterior, también hay que considerar que si el hilo a terminar está realizando alguna
operación larga, como acceso a red o a disco; lo mejor es implementar alguna forma de
saber cuando termine dicha operación y entonces solicitar su destrucción.
Inicialización del hilo
Por cierto, ¿ha notado que run no aparece en la lista anterior? Esto es porque nunca
deberemos llamar a run directamente. De hacerlo, se ejecutará el método en el mismo hilo
que en el que se hizo la llamada en lugar de hacerlo concurrentemente. Para ejecutar las
instrucciones de run apropiadamente usamos void Thread.start().
La palabra reservada synchronized
Ya hemos mencionado que los hilos se ejecutan concurrentemente y que comparten las
variables globales del programa. Los hilos pueden compartir otras cosas además de
variables globales y en general se denominan recursos compartidos a aquellos recursos del
sistema a los que la aplicación a la que tiene acceso por medio de más de un hilo (o
proceso).
Sin embargo, muchas veces vamos a querer organizar y restringir la manera en la que los
hilos interactúan con los recursos compartidos. Piense en el siguiente ejemplo: un programa
cuenta con dos hilos; uno realiza operaciones aritméticas y produce un resultado cada un
cierto tiempo no constante, y almacena el resultado en una variable compartida numérica. El
otro hilo le muestra al usuario los resultados que produce el primer hilo; ¿como organizamos
la ejecución de estos hilos para que el segundo evite mostrar resultados no actualizados?
¿como los sincronizamos?
A lo largo del curso, estudiaremos diversas estrategias y técnicas para resolver este tipo de
problemas y otros más; pero por ahora vamos a conocer uno de los mecanismos más
simples (desde el punto de vista del programador) que ofrece Java para sincronizar
aplicaciones concurrentes.
Para sincronizar varios hilos, es muy común usar “candados”. Estos candados se asocian a
los recursos compartidos, de forma que si están abiertos; el primer hilo que quiera
apropiarse del recurso, cierra el candado y obliga a esperar a todos los demás hilos que
quieran usar el recurso. Hasta que el hilo que cerró el candado lo vuelva a abrir, alguno de
los otros hilos podrá intentar apropiarselo para realizar sus tareas. Veremos a lo largo de
este curso que este problema no es trivial: bajo la solución expuesta no hay orden en quien
se apropia de los recursos y en sistemas multi-procesador; dos o más hilos pueden ser los
primeros en cerrar simultáneamente el candado.
En Java, todos los objetos cuentan con un candado (tentativamente cualquier objeto de
Java puede ser un recurso compartido). La palabra reservada de Java synchronized, tiene
dos sintaxis válidas (que nos ayudan a resolver bajo diferentes escenarios, el mismo
problema):
● Podemos usarla en la firma de un método después de la declaración de acceso
(public, private, protected...) y antes del tipo de datos del retorno (void, int, Object);
por ejemplo: public synchronized int metodo() { … }
En este caso; cuando los hilos intenten ejecutar el método síncrono, van a competir
por apropiarse del candado del objeto al que se le solicita ejecutar el método
síncrono (Java resuelve los problemas mencionados anteriormente y garantiza
exclusión mutua). Tome en cuenta que el candado es por objeto, por lo que si
tenemos dos instancias de la misma clase con el método sincronizado, y un hilo
solicita la ejecución del método en una de las instancias y otro hilo ejecuta el método
en la otra instancia; el método será ejecutado por ambos hilos en ambas instancias,
sin importar el estado de los candados entre ellos (porque son objetos diferentes).
Cuando el hilo que haya ejecutado el método síncrono termine de hacerlo, abre el
candado y le da oportunidad al resto de los hilos de ejecutar el método. Es
importante tomar en cuenta que por “hilo” no nos referimos exclusivamente a una
instancia de Thread; si no a cualquier objeto que ejecute sus instrucciones en algún
hilo de ejecución que pertenezca al programa.
●
Podemos usarla para definir un bloque de código sincronizado. Esta sintaxis
requiere del recurso compartido (algún objeto) que queremos manipular de forma
excluyente entre los hilos; por ejemplo:
synchronized(unObjeto) {
/*
* este bloque de código es el sincronizado
* y Java garantiza exclusión mutua en él
*/
}
Un último detalle que es importante tomar en cuenta sobre synchronized, es que al manejar
candados por objeto; cuando un hilo ejecuta un bloque de código síncrono, se apropia del
candado de dicho objeto y los demás hilos no sólo no podrán ejecutar simultáneamente ese
bloque de código síncrono particular; sino también ninguno otro que defina el mismo objeto
que se encuentre bloqueado.
Ejercicio. Revise el ejemplo “calendarizador en java” y observe la forma en la que varios
hilos realizan distintas operaciones por turnos de forma simultánea.
Thread pools (Grupos de hilos)
Los grupos de hilos son un tipo de objetos que tienen una colección de hilos listos para
ejecutarse. Son la forma preferida de crear hilos. Podemos crear hilos y grupos de hilos
mediante la clase java.util.concurrent.Executors; usando los métodos:
● newSingleThreadExecutor() - Devuelve uno y solo un hilo nuevo para realizar
tareas de segundo plano.
● newFixedThreadPool(int) - Crea un grupo de hilos con exactamente tantos hilos
listos para ejecutarse como se le indique en su parámetro.
● newCachedThreadPool() - Crea un grupo de hilos dinámico; es un poco menos
eficiente que el de tamaño fijo, pero es útil cuando no hay certeza del número de
hilos que va a necesitar nuestra aplicación (por ejemplo si es un servidor remoto).
Ahora bien, estos tres métodos (y muchos otros definidos en Executors) no devuelven
objetos (o colecciones) de la clase Thread o Runnable; sino más bien devuelven una
instancia de la clase java.util.concurrent.ExecutorService Esta clase es la que se
encarga de ejecutar los hilos previamente preparados de forma eficiente; y lo hace a través
del método execute(Runnable). Define algunos otros métodos que nos permiten un mayor
control e interacción con los resultados de los hilos en ejecución con los que contemos; pero
este será suficiente la mayor parte de las veces. Otro método importante en
ExecutorService es shutdown(). Este método interrumpe y termina la ejecución de
cualquier tarea que se le haya solicitado.
Callable y Future
Antes de retomar el uso de hilos en las GUI, vamos a conocer un par de formas más de
crear tareas concurrentes. La primera de ellas es a través de la interfaz
java.util.concurrent.Callable<V> Callable es muy similar a Runnable; excepto porque el
método que define (llamado call y no run) regresa un valor de tipo V, a diferencia de run que
no devuelve nada. Además, call puede disparar una excepción.
java.util.concurrent.Future es una interfaz que nos permite definir acciones mientras y al
final de la ejecución de tareas concurrentes. También puede ayudarles a ser interrumpidas
en caso de ser necesario.
Retomando el problema de la interfaz gráfica irresponsiva
Ahora conociendo los hilos en Java, podemos pensar en evitar obstruir el flujo del método
que responde a los eventos en el botón “Iniciar”. Quizá una primera idea para solucionar el
problema sea que el método instancie un hilo que contiene en su método run las
instrucciones de incrementar el contador y actualizar el texto.
Pero, ¿cómo hacemos que interactúe con el botón “detener”? Quizá lo más fácil sea contar
con una variable booleana global en el programa (recuerde que por omisión, todas las
variables globales de un programa son públicas para todos sus hilos). Al generar un evento
en “iniciar”, le asignamos un valor falso a la variable, e iteramos la cuenta mientras el valor
de dicha variable no cambie. Así, al generar un evento en “detener”, lo único que
tendríamos que hacer es justamente asignar el valor verdadero a la misma variable
compartida.
Ejercicio. Modifique el ejemplo “Interfaz no responsiva” de acuerdo a las ideas anteriores y
ejecute el programa. ¿el comportamiento de la aplicación es el esperado? ¿qué pasa con
los tiempos de respuesta?
Veamos algunas herramientas que nos proporciona Swing, para poder hacer una última
mejora a nuestro programa que nos permitirá aprovechar mejor los recursos del sistema.
Hilos y Swing
Toda aplicación que use Swing contará con al menos tres hilos:
1. Hilo principal, construido por la JVM cuando es invocado el programa para ejecutar
el main de nuestra aplicación. Este usualmente solo construye la interfaz gráfica y
muere; pero podríamos conservarlo para otras tareas, dependiendo del diseño de
nuestra aplicación.
2. Hilo administrador de eventos. Este es el encargado de refrescar la memoria de
vídeo y de administrar eventos; de forma que la entrada y salida de la aplicación (al
menos en lo que compete a la GUI) sea responsiva. Este hilo es el que ejecuta por
defecto todas las operaciones de los métodos receptores de eventos.
3. Hilos trabajadores de segundo plano, que se encargan de tareas de cómputo
intensivo e IO.
Por otro lado, la clase javax.swing.SwingUtilities contiene un par métodos estáticos de
interés para lo que estamos haciendo: static void invokeLater(Runnable) y static void
invokeAndWait(Runnable); que nos permite encolar los Runnables dados para ejecutar su
método run dentro del hilo de eventos de swing. La diferencia entre invokeLater e
invokeAndWait, es que invokeAndWait espera a que el hilo de eventos termine de ejecutar
todo lo que tenga pendiente (en la iteración en la que se encuentre), para al final ejecutar el
método run del Runnable dado.
Es muy recomendable usar invokeLater en el hilo principal de nuestro programa para
construir la interfaz gráfica.
Ya para terminar, conoceremos a la clase javax.swing.SwingWorker<T, V>; que nos
permite administrar los hilos de segundo plano en una aplicación gráfica. Como hemos visto
en el ejemplo de la interfaz no responsiva original; ejecutar código que requiera una fracción
considerable de tiempo en terminar en el hilo de eventos, es una mala estrategia porque da
la apariencia de que la aplicación se ha congelado.
Un SwingWorker es básicamente un hilo de ejecución que realiza tareas que toman mucho
tiempo en ejecutarse en segundo plano y devuelve los resultados al hilo de eventos para
que puedan reflejarse en la interfaz gráfica (recordemos que este último hilo es el
encargado de actualizar la memoria de vídeo). Sin embargo, su capacidad no se limita a
devolver los resultados al final de la ejecución; puede devolver también resultados parciales
que podemos usar para mostrarle al usuario los avances en pantalla.
SwingWorker es una clase que ocupa un par de tipos de datos parametrizados: T y V. T es
el tipo de datos que se le asociará a el resultado final de ejecutar las tareas del
SwingWorker y V es el tipo de datos que se asocia a los resultados parciales. SwingWorker
es una clase abstracta y para usarla necesitamos definir únicamente el método protected T
doInBackground(); que debe contener el código de la tarea larga que queremos que
realice el hilo de segundo plano.
Además, define varios otros métodos que podemos usar para conocer la interacción del
usuario con el sistema. Es recomendable que las tareas de cómputo intensivo en el método
doInBackground() se realicen iterativamente en un ciclo que se repita mientras el usuario no
desee interrumpir la tarea; para ello podemos apoyarnos del método isCancelled() como
condición de permanencia. Al final del ciclo es útil pasarle los resultados parciales del
procesamiento al método publish(V...), para exhibirlos al usuario. Es importante tomar en
cuenta que la publicación es asíncrona.
SwingWorker no sabe qué hacer por omisión con nuestros resultados parciales (ni con el
general), por lo que nos permite redefinir el método protected void
process(java.util.List<V>). La lista que recibe process como argumento, contendrá todos
los resultados parciales que se hayan acomulado durante un tiempo mientras se ejecuta
doInBackground (recordemos que estas publicaciones son asíncronas). Usando esta lista
podemos modificar elementos en la interfaz para reflejar el progreso. Es importante
considerar que cada vez que se invoca process, los resultados parciales que se hayan
recibido la última vez que se invocó process ya no estarán disponibles en la lista.
Finalmente; para actualizar la interfaz gráfica y reflejar los resultados generales de la
ejecución del SwingWorker, podemos sobreescribir el método protected void done(). Será
importante tomar en cuenta que done se ejecuta en el hilo de eventos de Swing, por lo que
debe ser breve o podría crear retrasos en la interfaz.
Ejercicio. Vuelva a modificar el ejemplo “Interfaz no responsiva”. Agregue una pequeña
espera (retraso) en el ciclo que cuenta y actualiza la etiqueta y termine el hilo al final del
método que atiende los eventos del botón “detener”. Analice y considere usar la la clase
SwingUtilities o SwingWorker para optimizar modificar la forma en la que se instancia el hilo
que realiza el conteo.
Referencias
1. Hock Chuan, C., (2008), “Java Programming Tutorial: Multithreading & concurrent
programming” en Nanyang Technological University. [En línea]. Singapur, disponible
en:
https://www3.ntu.edu.sg/home/ehchua/programming/java/J5e_multithreading.html
2. Oracle Corporation, “Java platform, standard edition 7. API specification” en Oracle
Help
Center.
[En
línea].
EE
UU,
disponible
en:
https://docs.oracle.com/javase/7/docs/api/ [Accesado el día 16 de agosto del 2016].