G.Bordel >Docencia >TAP Técnicas Actuales de Programación (curso 2010-2011)
desprotegido Intro. desprotegido Temario desprotegido Calendario desprotegido RPF desprotegido Recursos protegido Práctica protegido Gest. Alum.
tema_anterior Tema 10: Hilos tema_siguiente
  1. Introducción.
  2. Ciclo de vida de un hilo.
  3. Distribución de la CPU. Prioridades. El problema de la "inanición".
  4. Mecanismos de sincronización de hilos. El problema del interbloqueo.
  5. Agrupamientos de hilos.
  6. Estudio de un ejemplo de programación con hilos.[ejercicios]

10.2- Ciclo de vida de un hilo

Como se ha visto en el capítulo anterior, cada hilo de ejecución viene asociado a un objeto que comienza por instanciarse y posteriormente se pone en marcha mediante el método start. Estos son los dos primeros momentos en la existencia del hilo, una existencia que pasará por otros estados posteriores en función de diversos eventos y situaciones, como veremos a continuación. La siguiente figura representa estos diferentes estados que atraviesa el hilo a lo largo del tiempo. Es lo que se conoce como "ciclo de vida" del hilo.


.

Antes de comenzar a analizar estos estados, comentaremos que el procesador ejecuta trozos de código de diferentes hilos mediante un mecanismo de selección de la siguiente tarea a atender en cada momento que será objeto de estudio en el siguiente apartado (el "scheduling"). Este mecanismo selecciona una tarea de las que se encuentran referenciadas en una lista de candidatos. Cuando instanciamos un objeto hilo no esta dispuesto para ser ejecutado y por tanto no se encuentra en la mencionada lista (véase en la figura el estado "creado"). El método start es el que añade nuestro objeto a la lista y por tanto provoca que en algún momento posterior sea seleccionado para que el procesador ejecute un segmento de sus instrucciones (su estado es ahora "preparado"). Matizamos por tanto la afirmación anterior de que el método start arranca la ejecución del hilo diciendo que lo que hace es situarlo junto el resto de candidatos al uso del procesador y, en consecuencia, permite que arranque en cuanto el algoritmo de distribución de tiempos de CPU lo determine. En el momento en que el hilo es seleccionado para su ejecución sale de la lista para pasar al estado de "en ejecución" hasta que se le quite el procesador, cosa que puede suceder por diversos motivos llevándolo a diferentes estados.

Hay una característica que diferenicia dos tipos de algoritmos de distribución de CPU ("schedulers") que se denominan "preemptivos" y no "preemptivos". Los algoritmos "preemptivos" asignan un tiempo máximo a la ejecución de un hilo, y si esta no ha cedido el procesador en dicho tiempo (por el motivo que sea: terminación u otro), se le quita la CPU y se devuelve a la lista de candidatos. En cambio los algoritmos "no peemptivos" no tienen esa capacidad de retirada de la asignación de CPU por lo que son los hilos mismos los que deben ser programados de modo que no sean "egoistas" y cedan la CPU voluntariamente en cualquier segmento de código que potencialmente pueda presentar largo tiempo de ejecución. Estos distribuidores son claramente inferiores y por tanto han tendido a desaparecer con el tiempo. En las Máquinas Virtuales Java podemos encontrar distribuidores de los dos tipos en función de la plataforma sobre la que trabajemos, pero todas las razonablemente modernas presentan distribuidores "preemtivos" (los distribuidores no preemptivos de Java se conocen como "Green Threads" y quizás haya sido la propia SUN la última empresa en eliminarlos de sus plataformas con sistema operativo "Solaris"). Para hacer una cesión voluntaria de CPU se dispone del método yield, de modo que en un sistema no preemptivo deberiamos evitar los ciclos "egoistas" como se muestra (de un modo un tanto extremo) en el siguiente ejemplo:

1-  for (int i=1; i<100000;i++) {
2-   //instrucciones a realizar en el ciclo
3-   yield();
4-  }
En caso de que realicemos programas que puedan correr sobre plataformas que desconocemos (p.ej. applets que se ejecutarán en el navegador de cualquier usuario de Internet -ver segunda parte del curso-) hemos de tener en cuenta que cabe la posibilidad de que sean ejecutados por un distribuidor no preemptivo y por tanto, aun cuando no sea necesario en nuestra máquina, hemos de prevenir las situaciones de retención excesiva de CPU y evitarlas haciendo uso del yield.

Volviendo a los estados que presenta el hilo en el tiempo, en el caso más simple estos serán los ya vistos: inicialmente "creado" y posteriormente una sucesión de etapas en "preparado" y "ejecutándose" hasta que termine la tarea y concluya el método run pasando al estado "muerto". En este estado el objeto sigue existiendo pero la ejecución ha terminado. El objeto dejará de existir en caso de que se eliminen todas las referencias al mismo y el recolector de basuras restituya el espacio que ocupa a la memoria disponible.

Es posible forzar "la muerte" del hilo mediante el método stop, pero el uso de este método esta desaconsejado puesto que puede dar origen a problemas con la ejecución concurrente de los hilos, y en caso de que se prevea la posibilidad de abortar la ejecución de un hilo el método a utilizar consiste en la ejecución condicionada a una variable que pueda ser alterada:

1-  private boolean abortar=false;
2-  ...
3-  //rutina principal del hilo
4-  public void run() {
5-    ...
6-    while (!abortar) {
7-        //instrucciones a realizar en cada ocasión
8-    }
9-    ...
10-  }
11-  ...
12-  //rutina para abortar la ejecución del hilo
13-  public void abortar() {abortar=true;}
14-  ...

Para continuar con los estados que atraviesa el hilo nos queda ver aquellas situaciones en que este pierde la CPU por distintas causas que la cesión voluntaria mediante yield o la extinción del tiempo asignado por el distribuidor. Estas situaciones llevan en todo caso a estados de bloqueo, es decir estados en que el hilo no esta dispuesto a continuar ejecutándose mientras no desaparezca la causa por la cual se le ha quitado la CPU (y en consecuencia no va a la lista de candidatos). Hay dos estados de bloqueo diferentes: el estado "durmiendo" al que se llega voluntariamente mediante el método sleep cuando se determina que el proceso debe detenerse durante una determinada cantidad de tiempo, y el estado "en espera" al que se puede llegar voluntariamente mediante wait o involuntariamente cuando se pretende utilizar un elemento sobre el que no puede actuarse. Esto último requiere una explicación más detallada que veremos poco más adelante, antes de ello continuaremos con los estados del cilco de vida.

El método sleep(tiempo) se utilizará cuando sea preciso ejecutar un proceso de forma periodica cada cierto tiempo. Hay que tener en cuenta que su parámetro (en la versión más usual milisegundos) no determina exactamente el tiempo que transcurrirá hasta que se ceda de nuevo el procesador al hilo, sino que será el tiempo en el estado "durmiendo" y de este se pasará al estado "preparado" (a la lista de candidatos). Por tanto el periodo será más largo además de ligeramente variable. En caso de pretender una ejecución a intervalos fijos será necesario calcular los tiempos de espera de modo que, si bien no se consiga más regularidad si se consiga que no se acumulen desviaciones:

1-  final long periodo=1000L; //ejecución cada segundo
2-  long siguienteInstante=System.currentTimeMillis();
3-  long tiempo;
4-  
5-  while (hayaQueRepetir) {
6-      siguienteInstante+=periodo;
7-      //instrucciones a realizar en cada ocasión
8-      if((tiempo=siguienteInstante-System.currentTimeMillis())>0) {
9-         try {
10-             sleep(tiempo);
11-             } catch (InterruptedException ex) {/*acción pertinente*/}
12-          }
13-      else { yield(); }
14-  }

Como se aprecia en el ejemplo, esta espera puede ser interrumpida mediante una excepción y por tanto hemos de ejecutarlo en un bloque tray-catch-finalize (o declarar que el método arroja la excepción, si bien esto no es posible en el caso de que nos encontremos en el run ya que es el método que inicia el hilo y no es llamado por otro de nuestra responsbilidad. Esto esta controlado sintácticamente por el hecho de que el método run de Thread no arroja excepciones y por tanto no puede hacerse en su reescritura).

Los cambios de estado que nos quedan por ver son los pasos a "espera", que pueden ser voluntarios o no. En todo caso se trata de cuestiones relacionadas con la sincronización de las tareas llevadas a cabo por los hilos y los veremos con más detenimiento en un apartado posterior. Aquí los comentaremos sin profundizar demasiado.

La cesión voluntaria de CPU al estado de "en espara" se realiza mediante el método wait. Este es un mecanismo de sincronización entre hilos de modo que el tiempo es indefinido en espera a que se cumpla una determinada condición de la que avisará otro hilo mediante un método notify o notifyAll (p.ej. nuestro hilo espera a que otro genere un determinado dato que necesita y cuando este se genera es notificado). El método wait así como los notify y notifyAll son métodos de Object de modo que estan disponibles para cualquer objeto. En el caso del wait se dispone también de versiones con parámetros de tiempo, de modo que la espera puede disponer de un tiempo límite. Al igual que sucedia con el sleep, el wait tambien puede ser interrumpido y por tanto se habra de atender a dicho evento.

La cesión involuntaria se produce cuando un hilo en ejecución pretende realizar una acción sobre un objeto sobre el que estaba trabajando otro hilo cuando perdió el procesador y este tomo la precaución de reservarse el acceso para evitar que otros procesos interfirieran en su trabajo (veremos cómo más adelante). En este caso el hilo pasa al estado de espera y saldrá de él cuando el recurso sobre el que pretendia actuar quede liberado.

Siguiente punto: 10.3- Distribución de la CPU. Prioridades. El problema de la "inanición"


Plataforma de soporte a curso y contenidos (c) German Bordel 2005.