Está en la página 1de 9

Hilos de ejecución

Introducción
Los procesadores y los sistemas operativos actuales permiten la multitarea, esto es, la
realización simultánea de dos o más actividades (al menos en apariencia). En realidad,
un ordenador con una sola CPU no puede utilizar dos actividades a la vez (Muñoz Escoí,
Argente Villaplana, Espinosa Minguet, Galdámez Saiz, García-Fornes, De Juan Marín,
Sendra Roig, 2012). Sin embargo, los sistemas operativos son capaces de ejecutar
varios programas “simultáneamente” aunque solo se disponga de una CPU: reparten el
tiempo entre dos o más actividades o utilizan los tiempos muertos de una actividad, por
ejemplo, operaciones de lectura de datos desde el teclado, visto en nuestro caso
ejemplo de la lectura 1, para trabajar en la otra.

En ordenadores con dos o más procesadores, la multitarea es real, ya que cada uno
puede ejecutar un hilo o thread diferente.

1. Hilos
Los sistemas operativos evolucionaron para permitir que más de un programa se ejecute
a la vez, ejecutando programas individuales en procesos: programas aislados que se
ejecutan de forma independiente a los que el sistema operativo asigna recursos como
memoria, identificadores de archivos y credenciales de seguridad. Si fuera necesario, los
procesos podrían comunicarse entre sí a través de una variedad de mecanismos de
comunicación generales: sockets, manejadores de señales, memoria compartida,
semáforos y archivos.

Los siguientes factores motivadores llevaron al desarrollo de sistemas operativos que


permitieron que múltiples programas se ejecutarán simultáneamente: utilización de
recursos, fairness y conveniencia.

Utilización de recursos. A veces, los programas tienen que esperar a que se realicen
operaciones externas, como una entrada o una salida, y mientras esperan no pueden
realizar ningún trabajo útil. Es más eficaz utilizar ese tiempo de espera para dejar que se
ejecute otro programa.

Fairness (justicia). Múltiples usuarios y programas deben tener iguales prioridades


sobre los recursos de la máquina. Es preferible permitirles compartir la computadora a
través de una división de tiempo más fina que dejar que un programa se ejecute hasta
su finalización y luego iniciar otro.

Conveniencia. A menudo es más fácil o más deseable escribir varios programas para
que cada uno realice una sola tarea y hacer que se coordinen entre sí, según sea
necesario, que escribir un solo programa que realice todas las tareas.

Estos factores motivaron el desarrollo de hilos.

Entonces, podemos decir que un hilo o thread es un flujo secuencial simple


dentro de un proceso. Un único proceso puede tener varios hilos ejecutándose.
Un proceso es un programa ejecutándose de forma independiente y con un
espacio propio de memoria. Un sistema operativo multitarea es capaz de ejecutar
más de un proceso simultáneamente.

​Figura 1: Hilos y procesos


Fuente: [Imagen sin título sobre hilos]. (s.f.).

Los hilos son la unidad de planificación utilizada por el sistema operativo. Es decir, el
planificador toma sus decisiones con base en el conjunto de hilos de ejecución que se
mantengan en estado preparado (ready to run) en cada momento, escogiendo de
entre ellos al que estará en ejecución (running). Cada núcleo (o “core”) del procesador
no podrá tener más de un hilo en ejecución simultáneamente.

Un hilo se suspende cuando debe esperar a que suceda algún evento. Se dice entonces
que permanece en estado suspendido (blocked). Algunos ejemplos de eventos de
este tipo son: entrada desde teclado, lectura de un bloque del disco, llegada de un
paquete de información por la red, finalización de una operación de impresión sobre una
multifunción, etc. (Muñoz Escoí et al., 2012)

Cuando tal evento sucede, el sistema operativo reactiva al hilo que había quedado
suspendido previamente. Con ello, lo pasa al estado preparado. Dicho estado indica
que el hilo está listo para ser ejecutado. Sin embargo, todavía tendrá que esperar a que
el planificador lo escoja para que sea de nuevo ejecutado en algún procesador.

Los siguientes estados conforman el ciclo de vida de los hilos en una aplicación
realizada con el lenguaje de programación Java:

● nuevo (“new”): es el estado en el que se encontrará el hilo cuando acabe de ser


creado. Abandonará este estado cuando se invoque su método start(), pasando
entonces al estado preparado;
● preparado (“ready-to-run”): cuando nada obligue al hilo a realizar una espera y
pueda ser ejecutado. El único motivo por el que no estará en ejecución es que habrá un
número reducido de procesadores en el sistema y todos ellos estarán ocupados. Si en
un momento determinado existen varios hilos en estado preparado y alguno de los
procesadores del sistema queda libre, el planificador seleccionará cuál de ellos pasará a
ejecutarse (Muñoz Escoí et al., 2012). El criterio utilizado para realizar tal selección
dependerá del algoritmo de planificación utilizado en el sistema. En general, estos
algoritmos optimizan el rendimiento global o la utilización del procesador o el tiempo de
respuesta u otros parámetros. En cualquier caso, se intentará que el algoritmo sea lo
más equitativo posible (fairness), evitando así la inanición (esto es, que algún hilo
permanezca largo tiempo en la cola de preparados mientras otros vayan obteniendo el
procesador);

● en ejecución (“running”): un hilo permanece en este estado mientras sus


instrucciones sean ejecutadas por algún procesador.

Los hilos abandonan el estado de ejecución cuando hayan conseguido ejecutar todas
sus instrucciones (pasando entonces al estado terminado), cuando sean expulsados por
otros hilos (volviendo entonces al estado preparado), cuando utilicen el método
Thread.yield - es un método de los threads que permite indicarle al hardware que está
ejecutando la tarea, que puede interrumpirla y darle una oportunidad a otro thread - (que
también los deja en estado preparado) o cuando pasen a esperar la ocurrencia de algún
evento (Muñoz Escoí et al., 2012).

En caso de que se espere por algún motivo, el hilo suspenderá su ejecución y realizará
una transición a uno de estos tres estados: blocked, waiting o timed-waiting.

● Bloqueado (“blocked”): estado de suspensión en el que se trata de obtener algún


mecanismo de sincronización o bien se espera por la finalización de una operación de
E/S.
Los hilos abandonan este estado y pasan al estado preparado cuando termine la
operación de E/S solicitada o cuando obtengan el lock (Un “lock” es un objeto con dos
posibles estados (abierto o cerrado) y dos operaciones (abrir o cerrar el lock). Al crear el
lock, este se encontrará inicialmente abierto) o el acceso al método sincronizado por el
que se habían suspendido.

● Suspendido con temporización (“timed-waiting”): cuando el hilo se haya suspendido


debido a la utilización del método sleep() de la clase Thread. Este método admite como
argumento el número de milisegundos que el hilo tendrá que permanecer suspendido.
Los hilos abandonan este estado cuando finaliza el intervalo de suspensión que habían
especificado como argumento en su llamada a sleep() o bien si reciben una llamada a su
método interrupt(). En el primer caso, pasan a estar preparados. En el segundo caso,
reciben una InterruptedException, pasando a estado preparado o a estado terminado (si
no se gestionara la excepción) (Muñoz Escoí et al., 2012).

● Suspendido (“waiting”): cuando el hilo se haya suspendido debido a la utilización del


método join() (de la clase Thread, para esperar la finalización de otro hilo) o del método
wait() (de la clase Object, para esperar a que se cumpla determinada condición).
Cuando la suspensión fue originada por una llamada a wait() y termina debido a que otro
hilo utiliza notify() o notifyAll(), el estado de hilo previamente suspendido pasa a ser
bloqueado, ya que en ese caso competirá con el hilo notificador para obtener el derecho
a utilizar un método sincronizado. Esto último se explicará más en detalle en la lectura
sobre monitores.

● Terminado (“dead”): cuando el hilo finaliza su ejecución de todas sus instrucciones o


es interrumpido por una excepción que es incapaz de gestionar, pasa a este estado. Con
ello, el hilo es eliminado del sistema.

Cooperación entre hilos


La programación concurrente requiere que las distintas actividades que formen una
determinada aplicación cooperen entre sí para llevar a cabo la tarea para la que fue
diseñada tal aplicación. Revisaremos, a continuación, los mecanismos de cooperación
que podrán utilizarse para implantar una aplicación concurrente.
1) Comunicación: los mecanismos de comunicación tienen como principal objetivo que
las distintas actividades de una aplicación concurrente puedan intercambiar información
entre sí. Si no hubiera comunicación entre estas actividades, pasarían a ser
independientes y no se podría hablar de una aplicación concurrente. Estos mecanismos
de comunicación son los siguientes:

a) las variables compartidas: deben ser declaradas como globales, es decir, que
puedan ser utilizadas por todos los hilos del programa. De esa manera podrán
intercambiar información entre sí modificando alguna de las variables y leyendo el valor
modificado por otros hilos cuando sea conveniente;

b)intercambio de mensajes: permite comunicar tanto a hilos que estén en un mismo


proceso como a procesos ubicados en una misma máquina, o bien a procesos ubicados
en ordenadores diferentes.

2) Sincronización: la comunicación mediante variables compartidas deberá tener algún


método de sincronización. Los mecanismos de sincronización tienen como objetivo el
garantizar un determinado orden en la ejecución de ciertas sentencias o que se respeten
ciertas restricciones en la ejecución de determinadas secciones de código. Existen los
siguientes dos tipos básicos de sincronización:

a) exclusión mutua: una sección de código se ejecutará en exclusión mutua cuando


solamente un hilo pueda ejecutar dicha sección en cada momento;

b) sincronización condicional: este tipo de sincronización obliga a que los hilos se


suspendan, mientras no se cumpla una determinada condición. Dicha condición suele
depender del valor de algunas variables compartidas. Otros hilos, al modificar tales
variables conseguirán que finalmente se cumpla la condición, reactivando entonces a los
hilos previamente suspendidos (Muñoz Escoí et al., 2012).

Modelo de ejecución
Un hilo transforma su estado mediante la ejecución de sentencias. Una sentencia es una
secuencia de acciones atómicas que realizan transformaciones indivisibles.

Una acción atómica es aquella que transforma el estado y no puede dividirse en


acciones menores. Aquellas instrucciones máquina que no sean interrumpibles son
ejemplos válidos de acciones atómicas.

La transformación de estado realizada por una acción atómica no se ve afectada por


otras acciones, ya que dicha transformación no podrá ser interrumpida y cada uno de los
cambios parciales que provoque no resultarán visibles. Hasta que no termine por
completo dicha acción atómica, la transformación de estado que ella genere no será
visible.

Por tanto, para garantizar la coherencia en la ejecución de los métodos de un objeto


compartido, se deberán agrupar los pasos de los que consta esa transformación de
estado en una única acción atómica. De esa manera, se evitará que otros hilos puedan
acceder a los diferentes estados intermedios (e incoherentes) por los que transitará el
objeto.

Determinismo
Se dice que un programa es determinista cuando ante una misma combinación de datos
de entrada siempre (en cada una de las ejecuciones en las que se utilicen tales datos de
entrada) genera una misma salida.

Aunque se desconozca cómo se intercalarán las acciones atómicas ejecutadas por cada
hilo, ese no es motivo suficiente para asegurar que se perderá el determinismo en la
ejecución de un programa concurrente. La pérdida de determinismo se dará en caso de
que varios de esos hilos accedan a un mismo objeto compartido.

En ese caso, sí que será problemático el hecho de que se desconozca el orden concreto
en el que van a ejecutarse esas instrucciones, pues se podría provocar que alguna
actualización de ese objeto compartido no se aplicara de manera correcta, generando
inconsistencias.

La falta de determinismo (esto es, la existencia de condiciones de carrera) debería


evitarse, de la siguiente forma: las acciones ejecutadas por un hilo en cada ráfaga de
procesador siempre dejen a todos los objetos compartidos que hayan sido utilizados
durante el intervalo en un estado consistente. Con ello, los estados intermedios de estos
objetos compartidos no serían observados por otros hilos.

Esto solo lo puede garantizar el programador, utilizando mecanismos de sincronización.

Para considerar que un programa concurrente es correcto, se suelen exigir los


siguientes dos tipos de propiedades:

1) seguridad: no puede ocurrir nada incorrecto durante la ejecución. Para que se respete
la seguridad, se suelen exigir las siguientes dos condiciones:

a) exclusión mutua: cuando un hilo o actividad esté ejecutando el código de un objeto


compartido (es decir, alguno de sus métodos), ningún otro hilo o actividad podrá estar
ejecutando un método de ese mismo objeto;

b) ausencia de interbloqueos: los hilos que accedan a los recursos de una aplicación
concurrente (o del sistema en que esta se ejecute) no podrán quedar esperándose
mutuamente evitando así el avance de todos ellos;

2) vivacidad: en algún momento el programa proporcionará una salida y dicha salida


respetará la especificación del problema a resolver.
Para que se dé la propiedad de vivacidad, se suele asumir que se dan las siguientes dos
condiciones:

a) progreso: todo servicio solicitado se completa en algún momento. Es decir, no podrá


quedar ninguna petición o método iniciados pendiente de su compleción durante un
tiempo ilimitado;
b) equidad: todo hilo preparado pasará en algún momento a ejecución. Esto dependerá
del planificador utilizado en el sistema, pero se asumirá que este será equitativo,
evitando así que se retrase indefinidamente la entrada de un hilo en algún procesador.
Por ejemplo, los planificadores Round-Robin son equitativos y la mayoría de los
sistemas operativos modernos están basados en ese algoritmo (Muñoz Escoí et al.,
2012).

Referencias
Muñoz Escoí, F.D.; Argente Villaplana, E.; Espinosa Minguet, A.; Galdámez Saiz, P.;
García-Fornes , A.; De Juan Marín, R.; Sendra Roig, J. (2012). Concurrencia y
sistemas distribuidos. Universitat Politècnica.

También podría gustarte