Es posible implementar una biblioteca de hilos que soporte planificación preemptiva, rápidos cambios de contexto entre hilos, secciones críticas pequeñas, evite el crecimiento ilimitado de la pila, emplee pocas llamadas al sistema, y proporcione un interfaz independiente del lenguaje.
Un paquete de hilos consiste de un conjunto de primitivas relacionadas con la gestión de los hilos, y disponibles para que el programador realice aplicaciones multihilo. El paquete de hilos también se encargará de gestionar, controlar, crear y destruir los hilos que la aplicación multihilo requiere para su ejecución. Los paquetes de hilos suelen tener también llamadas para especificar el tipo de algoritmo de planificación deseado para los hilos: round-robin, prioridades, etc., así como establecer las prioridades en su caso.
Para maximizar el rendimiento de la biblioteca de hilos, las llamadas al kernel del sistema operativo deben ser minimizadas. La sobrecarga asociada cuando se entra y se sale del kernel del sistema hace que las llamadas al sistema sean operaciones muy costosas. La mayoría de las llamadas que debe realizar el paquete estarán relacionadas con la inicialización de la biblioteca y otra serie de etapas que no sean críticas en tiempo.
Existen dos formas de gestionar los hilos:
a) Diseño de Hilos Estáticos : El numero de hilos se fija al escribir el programa o durante la compilación. Cada hilo tiene asociada un pila fija. Es una alternativa simple pero inflexible.
b) Diseño de Hilos Dinámicos : Se permite la creación y destrucción de hilos durante la ejecución. En este modelo un proceso se inicia con un sólo hilo (de manera implícita), pero puede crear más hilos posteriormente. Este suele ser el diseño habitual empleado en los paquetes de hilos actuales.
Existe una llamada para la creación de hilos que determina el programa principal del hilo, para ello se pasa a la llamado un puntero al procedimiento principal, un tamaño para la pila del hilo, y otra serie de parámetros, como puede ser la prioridad de planificación. La llamada devuelve un identificador del hilo. La sintaxis abstracta de la llamada de creación de hilos es id_Hilo CreateThread(puntero_procedimiento, tamaño_pila, Parametros...)
Un hilo puede finalizar su ejecución por dos razones (al igual que un proceso) : la terminación natural de su trabajo o su eliminación desde el exterior.
El estándar POSIX Threads especifica un modelo de hilos dirigido por prioridades con políticas de planificación preemptivas, gestión de señales, y primitivas para proporcionar exclusión mutua así como espera sincronizada. El diseño de un paquete de hilos suele estar influenciado por las restricciones impuestas por el estándar POSIX Threads (si se desea seguir dicho estándar, caso habitual) y el tipo de plataforma sobre la que se va a implementar. Normalmente el diseño del paquete intenta reducir al mínimo la cantidad de código dependiente de la plataforma, y se suele aislar en módulos concretos, de forma que la biblioteca sea adaptable a nuevas plataformas.
En la mayoría de las implementaciones, el
interfaz consiste en una biblioteca C con puntos de entrada enlazables,
de forma que pueda ser compilada para generar un interfaz de lenguaje
independiente. Este es el caso de la implementación Pthreads
realizada por Frank Mueller como base del proyecto PART (Portable
Ada Run-Time), en el que se implementa un sistema de tiempo de
ejecución para la gestión de tareas Ada apoyado
en la biblioteca de hilos.
Un interfaz permite que los programas empleen los servicios de la biblioteca independientemente del lenguaje que empleemos. En el caso del lenguaje C, las rutinas de la biblioteca son utilizables directamente, mientras que en otros lenguajes es necesario crear un interfaz entre el lenguaje y la biblioteca de hilos.
En algunos diseños de paquetes de hilos, el código de las rutinas de la biblioteca se ejecuta normalmente en modo usuario, aunque las secciones críticas se ejecutan en un modo protegido o kernel de la biblioteca (no confundir con el modo kernel del sistema), que es un modo especial que garantiza la exclusión mutua entre hilos. Estas implementaciones suelen emplear rutinas de la biblioteca estándar del sistema y algunas llamadas al kernel del sistema.
Los objetivos de diseño que deben prevalecer son :
Como los hilos comparten el espacio de direcciones, comparten las variables globales. El acceso a dichas variables se realiza generalmente mediante regiones críticas, para evitar que varios hilos tengan acceso a los mismos datos al mismo tiempo. Por ello es necesario sincronizar el acceso a los recursos compartidos por los hilos de un proceso. La implementación de las regiones críticas es más sencilla utilizando semáforos, monitores u otras construcciones similares.
El estándar POSIX Threads especifica un mútex como estructura de datos para garantizar el acceso exclusivo a datos compartidos, y variables de condición para realizar la sincronización entre hilos. Otros métodos de sincronización tales como semáforos contadores pueden implementarse empleando estas primitivas. Normalmente los paquetes de hilos implementan las siguientes técnicas :
Un mútex consiste en una especie de semáforo binario con dos estados, cerrado y no cerrado. Un mútex es un objeto que permite a los hilos asegurar la integridad de un recurso compartido al que tienen acceso. Tiene dos estados : bloqueado y desbloqueado. Sobre un mútex se pueden realizar las siguientes operaciones:
Antes de acceder a un recurso compartido un hilo debe bloquear un mútex. Si el mútex no ha sido bloqueado antes por otro hilo, el bloqueo es realizado. Si el mútex ha sido bloqueado antes, el hilo es puesto a la espera. Tan pronto como el mútex es liberado, uno de los hilos en espera a causa de un bloqueo en el mútex es seleccionado para que continúe su ejecución, adquiriendo el bloqueo.
Un ejemplo de utilización de un mútex es aquél en el que un hilo A y otro hilo B están compartiendo un recurso típico, como puede ser una variable global. El hilo A bloquea el mútex, con lo que obtiene el acceso a la variable. Cuando el hilo B intenta bloquear el mútex, el hilo B es puesto a la espera puesto que el mútex ya ha sido bloqueado antes. Cuando el hilo A finaliza el acceso a la variable global, desbloquea el mútex. Cuando esto suceda, el hilo B continuará la ejecución adquiriendo el bloqueo, pudiendo entonces acceder a la variable.
Hilo A : Hilo B :
lock(mutex) lock(mutex) acceso al recurso acceso al recurso unlock(mutex) unlock(mutex)
Un hilo puede adquirir un mútex no bloqueado. De esta forma, la exclusión mutua entre hilos del mismo proceso está garantizada, hasta que el mútex es desbloqueado permitiendo que otros hilos protejan secciones críticas con el mismo mútex. Si un hilo intenta bloquear un mútex que ya está bloqueado, el hilo se suspende. Si un hilo desbloquea un mútex y otros hilos están esperando por el mútex, el hilo en espera con mayor prioridad obtendrá el mútex. Para simplificar las implementaciones, normalmente un hilo no puede ser cancelado cuando tiene cancelación activada y controlada, y se suspende debido a la espera del mútex, con el fin de garantizar un estado determinista del mútex en los gestores de limpieza.
Un mútex debe ser bloqueado durante el menor tiempo posible para minimizar la espera. Por ejemplo, se debe proteger el acceso a estructuras de datos compartidas entre hilos mediante mútex. Pero no se debe bloquear un mútex, realizar una acción que puede provocar la suspensión del hilo, y después desbloquear el mútex, porque se puede producir la espera durante un largo periodo mientras se posee el mútex.
Una variable de condición es un objeto que permite que los hilos se ejecuten en un secuencia ordenada. Los hilos pueden esperar a que se cumpla una cierta condición, o pueden indicar a otros hilos que se ha producido una cierta condición, liberando uno o más hilos de la condición de espera. Existe un mútex y un predicado asociados con una variable de condición. El predicado es una variable específica de la aplicación comprobada por los hilos, indicando que algún tipo de condición está lista o no. El predicado es necesario porque la variable de condición misma es opaca, siendo usado en las operaciones wait y signal. El mútex de la variable de condición asegura que un hilo como máximo está accediendo al predicado y entrando en un estado de espera o enviando una señal basada en su valor.
Típicamente, una variable de condición es empleada para indicar cuando un hilo debe comenzar a trabajar en una tarea. Esto puede ser útil para implementar el modelo de hilos de trabajo en equipo, haciendo que los hilos trabajadores esperen por una variable de condición usada como un flag para indicar el instante de comienzo. En el modelo de tubería se puede emplear una variable de condición para indicar que la etapa anterior de una tarea ha sido completada por algún hilo.
Un ejemplo de utilización de una variable de condición es aquel en el que un hilo A bloquea un mútex asociado con una variable de condición. Eventualmente el hilo A adquiere el bloqueo de este mútex, procediendo entonces a acceder al predicado. Si el predicado indica un estado deseado, el hilo A procederá a desarrollar la tarea X y desbloqueará el mútex. Si no, el hilo A entrará en un estado de espera, pero la operación wait realizará automáticamente el desbloqueo del mútex. El hilo B bloquea el mútex asociado con la variable de condición después de haber completado la tarea Y. Eventualmente, el hilo B adquirirá el bloqueo de este mútex, procediendo entonces a acceder al predicado. El hilo B pondrá el predicado en el estado ready, indicando con signal que el predicado está listo y desbloqueando el mútex. Si el hilo A estaba esperando en la variable de condición, la indicación signal emitida por el hilo B provocará que el hilo A continúe su ejecución. Supongamos la secuencia en la que el hilo A sólo realiza la tarea X después de que el hilo B haya realizado la tarea Y. El hilo B indica que ha realizado la tarea Y empleando una variable de condición accedida por ambos hilos A y B.
Hilo A : Hilo B :
lock(mutex) Realizar Tarea Y while (!predicado) lock(mutex) wait(var_condic, mutex) predicado = TRUE Realizar Tarea X signal(var_condic) unlock(mutex) unlock(mutex)
Para permitir la sincronización entre hilos y la suspensión durante un largo intervalo de tiempo, se emplean las variables de condición. Una variable de condición está asociada con un mútex y un predicado basado en datos compartidos. Cuando un hilo necesita sincronizarse con otro, bloquea el mútex, comprueba el predicado y, si el predicado se evalúa a falso, se suspende en la variable de condición. Cuan el hilo es reactivado, vuelve a evaluar el predicado y así hasta que el predicado se hace cierto. La evaluación nuevamente del predicado es esencial ya que las reactivaciones en un multiprocesador y las reactivaciones debidas a eventos asíncronos pueden causar que el hilo continúe su ejecución mientras el predicado permanece a falso, debido a que las operaciones no son atómicas.
Cuando un hilo entra en una espera condicional con el mútex asociado bloqueado, el mútex es desbloqueado y el hilo suspendido en una sola operación atómica. De forma similar, el mútex es bloqueado nuevamente cuando el hilo continua su ejecución, de forma atómica. Así pues, el mútex asociado está siempre en un estado conocido, incluso cuando las señales interrumpen una espera condicional, ya que el mútex es readquirido antes de que se inicie la ejecución de cualquier gestor de interrupciones.
Una variable de condición es "señalada" por un hilo después de que el hilo cambie el estado de algún dato compartido permitiendo que el predicado asociado se haga cierto. Cuando una variable de condición es señalada, al menos uno de los hilos bloqueados en ella pasa al estado preparado. Si más de un hilo está bloqueado en la variable de condición, el hilo con mayor prioridad pasará al estado preparado. En particular, en un sistema multiprocesador, puede ser más eficiente desbloquear más de un hilo a la espera de la condición.
Una variable de condición es una variable similar a la variable de condición empleada en la implementación de los monitores como técnica de sincronización. Se suele asociar un mútex a una variable de condición cuando ésta se emplea. Cuando se espera por una variable de condición, se indica el mútex bloqueado asociado a la sección crítica a la que se desea acceder cuando se cumpla la condición. De esta forma la variable de condición y el mútex cooperan en la protección de una región crítica condicional.
- El mútex se emplea para realizar un cierre a corto plazo, normalmente para proteger el acceso a las regiones críticas.
- La variable de condición se utiliza para una espera a largo plazo, en espera de algún recurso no disponible. Por ello cuando se espera por una condición, se libera el mútex de acceso a la región critica, pero se vuelve a bloquear dicho mútex cuando se despierta la variable de condición.
El problema de compartir las variables globales por varios hilos puede provocar que generemos valores incorrectos en sus valores, debido a la indeterminación del hilo que se ejecutará en cada momento. Existen diversas soluciones a este problema:
1) Prohibir las variables globales: Es algo ideal, pero genera conflictos con el software existente.
2) Asignar variables globales particulares a cada hilo: Cada hilo tiene su propia copia de la variable global, lo que evita los conflictos. Se crea pues un nivel más de visibilidad correspondiente a las variables visibles por todos los procedimientos de un hilo. El problema es que no todos los lenguajes tienen formas de expresar este tipo de variables. Una solución es asignar un bloque de memoria a las variables globales y transferirla a cada procedimiento del hilo como un parámetro adicional. No es una solución elegante, pero funciona.
3) Crear nuevos procedimientos de biblioteca, para crear, asignar y leer los valores de estas variables globales en un hilo. Esta es la forma que suelen adoptar la mayoría de los paquetes de hilos. Las llamadas, de forma abstracta, podrían ser:
- create_global("puntero_variable") = Asigna un espacio de almacenamiento para un puntero de una variable en un área especial reservada para el hilo que hizo la llamada. El hilo que hizo la llamada es el único que puede acceder a la variable. Si otro hilo crea una variable global con el mismo nombre obtiene un espacio de almacenamiento distinto, lo que evita conflictos con el ya existente.
- set_global("puntero_variable", &valor) = Almacena el valor de un puntero en el espacio de almacenamiento reservado anteriormente.
- puntero_variable = read_global("puntero_variable")
= Devuelve la dirección almacenada en la variable global,
para tener acceso al valor del dato.