Notas de clase. Threads

Licenciatura en Ciencias de la Computación, Facultad de Ciencias, UNAM. Computación concurrente. Profesor: Carlos Zerón Martínez. Ayudante: Manuel Ign

5 downloads 148 Views 112KB Size

Recommend Stories


Econometria Notas de clase
Econometria Notas de clase Walter Sosa Escudero Nota preliminar: estas Notas de Clase sirven al unico proposito de apoyar el dictado de cursos de eco

Sistemas No Lineales. Notas de Clase
Sistemas No Lineales Notas de Clase Por Mar´ıa Marta Seron Laboratorio de Sistemas Din´ amicos y Procesamiento de Se˜ nales (LSD) Universidad Naciona

Ecuaciones estocásticas y fluidos: Notas de clase
Ecuaciones estoc´asticas y fluidos: Notas de clase Rafael Granero Belinch´on1 26 de abril de 2011 Resumen Notas de una serie de clases impartidas en

AGRO 5005 BIOMETRÍA. Notas de clase
AGRO 5005 BIOMETRÍA Notas de clase 2015 Raúl E. Macchiavelli, Ph.D. Linda Wessel-Beaver, Ph.D. Estas notas complementan el material presentado en el

The Threads of Memory
Madrid 17 de febrero de 2010. TEXTO COLEGIO DE INGENIEROS DE CAMINOS, CANALES Y PUERTOS. El Hilo de la Memoria. / The Threads of Memory. LOS PRIMERO

INMUNOLOGÍA APLICADA. Notas de Clase. María Dolores Lastra A. INMUNOHEMATOLOGIA
INMUNOLOGÍA APLICADA Notas de Clase María Dolores Lastra A. INMUNOHEMATOLOGIA En 1901 Karl Landsteiner demostró la existencia de los antígenos de los

Story 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 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; 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). 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].

Get in touch

Social

© Copyright 2013 - 2024 MYDOKUMENT.COM - All rights reserved.