G.Bordel >Docencia >TAP | Técnicas Actuales de Programación | (curso 2010-2011) | |||||||
|
|||||||||
tema_anterior | Tema 10: Hilos | tema_siguiente |
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:
|
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:
|
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:
|
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. |