Pr ogr amac ión A v anz ada Hilos Hi los 1. Fundamentos. Un programa multihilo contiene dos o más partes que pueden ejecutarse de forma concurrente. Cada parte de ese programa se llama hilo (Thread) y cada hilo establece un camino de ejecución independiente (multitasking). Los sistemas de un solo hilo utilizan un enfoque llamado bucle de suceso con sondeo. Cuando un hilo se bloquea deteniendo su ejecución, el programa completo se detiene. Existen dos tipos distintos de multitarea: 1.- Basada en procesos: Permite a la computadora ejecutar más de un proceso concurrentemente. La unidad de código más pequeña es el proceso. Los procesos son tareas pesadas que necesitan su propio espacio de direccionamiento. La comunicación entre procesos es cara y limitada. 2.- Basada en hilos: Un programa simple puede realizar más de una tarea simultáneamente. La unidad de código más pequeña es el hilo. Los hilos (procesos ligeros) son más ligeros que los procesos (procesos pesados). Comparten el mismo espacio de direcciones y el mismo proceso pesado. La comunicación entre hilos es ligera y el cambio contexto de un hilo al siguiente es menos costoso. Java hace uso de entornos multitarea basados en procesos, pero la multitarea basada en hilos está bajo el control de Java. 2. El modelo de hilo de Java. Un hilo puede estar en uno de los siguientes estados: Ejecutándose. Preparado para ejecutarse, tan pronto como se disponga de tiempo de CPU. Suspendido , su ejecución se detiene temporalmente. Reanudarse, permitiendo que continúe su tarea donde la dejó. Bloqueado , cuando está a la espera de algún recurso. El intérprete de Java utiliza las prioridades para determinar cómo debe tratar cada hilo con respecto a los demás. La prioridad de un hilo se utiliza para decidir cuándo se pasa a ejecutar otro hilo (cambio de contexto). Las reglas que determinan un cambio de contexto son las siguientes: Flo r ent ino Fdez. Rivero la Dpto . I nfo rm át ica UN IVE RSIDA D DE VIG O 1 Pr ogr amac ión A v anz ada • • • Hilos Un hilo puede ceder voluntariamente el control por abandono explícito, al quedarse dormido o al bloquearse en espera de una operación de E/S pendiente. => Se examinan los hilos restantes y se selecciona aquel que tenga mayor prioridad. Un hilo que no libera la CPU puede ser desalojado por otro de mayor prioridad. => (multitarea por desalojo). En el caso de hilos de igual prioridad que compiten por la CPU, depende del S.O. (¡precaución!).En Windows 95 los hilos de igual prioridad se reparten la CPU mediante un algoritmo circular (round-robin). En Solaris 2.x los hilos de igual prioridad deben ceder el control voluntariamente, sino los demás hilos no se ejecutarán. SINCRONIZACIÓN Java implementa una versión de un modelo clásico de sincronización entre procesos, llamado monitor (definido inicialmente por Hoare, y que puede entenderse como una pequeña caja negra en la que sólo cabe un hilo). Los monitores se utilizan para proteger un recurso compartido y evitar que sea manipulado por más de un hilo simultáneamente. En Java cada objeto tiene su propio monitor, en el que se entra cuando se llama a uno de los métodos sincronizados del objeto. Una vez que un hilo está dentro de un método sincronizado, ningún otro hilo puede llamar a otro método sincronizado del mismo objeto. INTERCAMBIO DE MENSAJES Java proporciona métodos predefinidos que permiten que un hilo entre en un método sincronizado de un objeto, y espere ahí hasta que otro hilo le notifique explícitamente que debe salir de él. LA CLASE TREAD Y LA INTERFAZ RUNNABLE Para crear un nuevo hilo en Java, el programa deber heredar de la clase Thread o implementar la interfaz Runnable. Los métodos básicos para gestionar los hilos se enumeran a continuación: public final String getName() Obtiene el nombre de un hilo. public final int getPriority() Obtiene la prioridad de un hilo. public final native boolean isAlive() Comprueba si un hilo se está ejecutando todavía. Devuelve true si todavía está ejecutándose o false en caso contrario. Flo r ent ino Fdez. Rivero la Dpto . I nfo rm át ica UN IVE RSIDA D DE VIG O 2 Pr ogr amac ión A v anz ada Hilos public final void join() throws InterruptedException Espera hasta que finalice el hilo sobre el que se llama. public final void resume() Reanuda la ejecucuión de un hilo suspendido public void run() Punto de entrada de un hilo. public static native void sleep(long millis) throws InterruptedException Suspende un hilo durante un período de tiempo public native synchronized void start() Comienza un hilo llamando a su método run(). public final void stop() Detiene la ejecución de un hilo. public final void suspend() Suspende un hilo. 3. El hilo principal. Creación de un hilo. Cuando se ejecuta un programa Java ya existe un hilo en ejecución, llamado hilo principal. Este hilo es especial por dos razones: • • Desde él se crearán el resto de hilos del programa. Debe ser el último hilo que termine su ejecución. (Si un hilo principal finaliza antes que un hijo Java puede bloquearse, hang). El método run() es el punto de entrada de un nuevo hilo de ejecución concurrente dentro de un programa. El hilo termina cuando finalice el método run(). Para que un hilo comience su ejecución se debe llamar a su método start(). La mayoría de los programadores en Java crean hilos heredando de la clase Thread cuando modifican o mejoran dicha clase. Si lo que se necesita es simplemente la funcionalidad de un hilo, lo normal es implementar la interfaz Runnable. 4. Creación de múltiples hilos. isAlive(), join(), suspend(), resume(). Los programas en Java pueden generar tantos hilos como necesiten. Para asegurarse de que un hilo hijo ha terminado se pueden utilizar dos métodos: isAlive() y join(). Flo r ent ino Fdez. Rivero la Dpto . I nfo rm át ica UN IVE RSIDA D DE VIG O 3 Pr ogr amac ión A v anz ada Hilos Se puede pasar un hilo que está ejecutándose al estado “suspendido” mediante la llamada al método suspend(), para reanudarlo se llama al método resume(). Esto puede ser útil por ejemplo, en el caso de un hilo que implemente un reloj en pantalla. Si el usuario no desea el reloj, el hilo puede suspenderse. 5. Prioridades. setPriority(), getPriority(). La manera en la que un S.O. implementa la multitarea puede afectar al tiempo de CPU disponible para cada hilo. Si se desea lograr un comportamiento predecible sobre distintas plataformas, se deben programar hilos que voluntariamente cedan el control a la CPU. En concreto, los hilos que comparten la misma prioridad deben ceder el control de vez en cuando para asegurar que todos los hilos tengan oportunidad de ejecutarse en un S.O. con multitarea no apropiativa (non-preemptive). Cuando está ejecutándose un hilo de baja prioridad y otro de mayor prioridad se despierta de su sueño o de la espera de cierta operación de E/S, el hilo de menor prioridad es desalojado. Para asignar una prioridad a un hilo se utiliza el método setPriority(). Los posibles valores están comprendidos entre MIN_PRIORITY (1) y MAX_PRIORITY (10). La constante NORM_PRIORITY (5) establece la prioridad por defecto. Para obtener la prioridad de un hilo se utiliza el método getPriority(). 6. Sincronización. La sentencia synchronized. Cuando dos o más hilos necesitan acceder de manera simultánea a un recurso compartido, necesitan asegurarse de que sólo uno de ellos accede al mismo en un instante dado. El hecho de que varios hilos llamen al mismo método sobre el mismo objeto a la vez se denomina “condición de carrera” (race codition). En C/C++ la sincronización de procesos se realiza mediante llamadas a primitivas del sistema operativo. Java implementa la sincronización a través de elementos del propio lenguaje (métodos synchronized). Durante todo el tiempo que un hilo permanezca dentro de un método sincronizado, los demás hilos que intenten acceder a ese método sobre la misma instancia, tendrán que esperar. Para sincronizar el acceso a objetos de una clase que no fue diseñada para acceso multihilo, se realizan las llamadas a los métodos dentro de un bloque sincronizado: synchronized(objeto) { Flo r ent ino Fdez. Rivero la Dpto . I nfo rm át ica UN IVE RSIDA D DE VIG O 4 Pr ogr amac ión A v anz ada Hilos // sentencias que deben ser sincronizadas; } 7. Comunicación entre hilos. Java proporciona un mecanismo de comunicación entre procesos a través de los métodos wait(), notify(), y notifyAll() definidos en la clase Object (todas las clases disponen de ellos). Cualquiera de estos tres métodos sólo puede ser llamado desde dentro de un método synchronized. public final void wait() throws InterruptedException Le indica al hilo en curso que abandone el monitor y se vaya a dormir hasta que otro hilo entre en el mismo monitor (algún método sinchronized de la misma clase) y llame a notify(). Existen otras formas para este método donde se permite especificar un período de tiempo de espera. public final native void notify() Despierta al primer hilo que realizó una llamada a wait() sobre el mismo objeto. public final native void notifyAll() Despierta todos los hilos que realizaron una llamada a wait() sobre el mismo objeto. El hilo con mayor prioridad de entre los que han sido despertados es el primero en ejecutarse. Un buen ejemplo es el problema del productor/consumidor modificado de forma que el productor tiene que esperar a que el consumidor haya terminado para empezar a producir más datos. 8. Bloqueos. deadlock. Este tipo de error está relacionado con la multitarea y es necesario evitarlo. Se produce cuando dos hilos tienen una dependencia circular en un par de objetos sincronizados. El bloqueo es un error difícil de resolver por dos razones: • • Ocurre en raras ocasiones, cuando los dos hilos intentan entrar a la vez. En el bloqueo pueden estar involucrados más de dos hilos y dos objetos sincronizados. Flo r ent ino Fdez. Rivero la Dpto . I nfo rm át ica UN IVE RSIDA D DE VIG O 5