¿ Qué es Pthreads ?
Pthreads es la abreviatura de POSIX threads. Es el estándar sobre hilos IEEE POSIX1003.1c, aprobado en Junio de 1995. También es la abreviatura de la implementación del estándar POSIX diseñada por Chris Provenzano, desarrollada en el MIT, y que se presenta en este manual.
El paquete de hilos que se presenta es una implementación del estándar POSIX Threads en el nivel de usuario. Lo que se proporciona al usuario es una librería de programación para el lenguaje C, que le permitirá desarrollar aplicaciones empleando las técnicas de programación relacionadas con los hilos siguiendo el estándar POSIX.1c, recientemente aprobado.
El paquete está basado en el borrador del estándar sobre hilos POSIX 1003.1c Draft 10, y el informe sobre gestión de señales presentado por Frank Muellers en la Winter 93 USENIX Conference. Actualmente está siendo escrito y diseñado por Chris Provenzano en el MIT (Massachussets Institute of Technology)., puesto que su desarrollo continua en vigor.
La principal ventaja de realizar programas empleando
Pthreads es la portabilidad de la aplicación a otras plataformas
que ejecuten otros paquetes que sigan el estándar POSIX.
El paquete proporciona las herramientas típicas de la programación
de hilos como son : gestión de hilos, mútex,
variables de condición, datos específicos de hilo,
etc.
Los hilos son frecuentemente llamados procesos ligeros, aunque este término es una simplificación del concepto de hilo. Los hilos son primos de los procesos UNIX, aunque no son realmente procesos. Para comprender las diferencias se pueden observar las relaciones entre los procesos de UNIX y las tareas (procesos) de Mach :
Como los hilos son más pequeños comparados con los procesos, la creación de un hilo es relativamente menos costosa. Como los procesos requieren su propio conjunto de recursos, y los hilos comparten los recursos, los hilos son más económicos en relación a la memoria empleada. Los hilos proporcionan a los programadores la posibilidad de escribir aplicaciones concurrentes que se pueden ejecutar tanto en sistemas monoprocesador como multiprocesador de forma transparente, obteniendo un aumento de rendimiento considerable cuando se encuentran disponibles procesadores adicionales. Además, los hilos pueden incrementar el rendimiento en un entorno monoprocesador cuando la aplicación realiza operaciones típicas de bloqueo o produce retrasos, como E/S de ficheros o sockets.
A continuación se presenta una introducción
a la programación concurrente con hilos. Un paquete de
hilos permite escribir programas con varios puntos simultáneos
de ejecución, sincronizándose a través de
memoria compartida. Se describen las primitivas básicas
de hilos y sincronización, y se explica como se pueden
emplear, los problemas que pueden ocasionar, y como evitar estos
problemas. Se ha enfocado de cara a programadores experimentados
que quieren adquirir experiencia práctica en la escritura
de programas concurrentes con hilos.
Muchos de los nuevos sistemas operativos tanto experimentales como comerciales han incluido recientemente soporte para programación concurrente. El mecanismo más popular para ello ha sido la introducción de múltiples hilos de ejecución ligeros con un espacio de direcciones común, empleados dentro de un programa.
La programación con hilos introduce nuevas dificultades incluso para programadores experimentados. La programación concurrente tiene técnicas y problemas que no ocurren en la programación secuencial. Muchas de las técnicas son obvias, pero algunas sólo son obvias con una visión a posteriori. Algunos de los problemas son sencillos (por ejemplo, el bloqueo mutuo es un "agradable" tipo de error, el programa se detiene con todas las evidencias intactas), pero algunos aparecen como penalizaciones al rendimiento. A continuación se desarrolla una introducción a las técnicas de programación que funcionan bien con los hilos, y se avisa de aquellas técnicas que no son adecuadas. El objetivo es que el programador secuencial experimentado sea capaz de construir un programa multihilo que funcione correctamente, eficientemente y con un mínimo de sorpresas.
Un hilo (thread) es un concepto sencillo : un simple flujo de control secuencial. En un lenguaje de alto nivel normalmente se programa un hilo empleando procedimientos, donde las llamadas a procedimientos siguen la disciplina tradicional de pila. Con un único hilo, existe en cualquier instante un único punto de ejecución. El programador no necesita aprender nada nuevo para emplear un único hilo. Cuando se tienen múltiples hilos en un programa significa que en cualquier instante el programa tiene múltiples puntos de ejecución, uno en cada uno de sus hilos. El programador puede ver los hilos como si estuviesen en ejecución simultánea, como si el ordenador tuviese tantos procesadores como hilos en ejecución. El programador decide cuando y donde crear múltiples hilos, ayudándose de un paquete de librería o un sistema de tiempo de ejecución. Adicionalmente, el programador puede ser consciente de que el ordenador no ejecuta todos los hilos simultáneamente.
Los hilos se ejecutan en un único espacio de direcciones, lo que significa que el hardware de direccionamiento del ordenador está configurado para permitir que los hilos lean y escriban en las mismas posiciones de memoria. En un lenguaje de alto nivel, esto corresponde normalmente al hecho de que las variables globales (fuera de la pila) son compartidas por todos los hilos del programa. Cada hilo se ejecuta en una pila separada con su propio conjunto de variables locales. El programador es el responsable de emplear los mecanismos de sincronización del paquete de hilos para garantizar que la memoria compartida es accedida de una forma correcta. Las facilidades proporcionadas por el paquete de hilos son conocidas como "primitivas ligeras". Esto significa que las primitivas de creación, mantenimiento, destrucción y sincronización son suficientemente económicas en esfuerzo para las necesidades de concurrencia del programador.
A continuación se emplea el término "proceso" sólo cuando nos refiramos a un único flujo de control con un espacio de direcciones propio. Debe quedar claro que los hilos no son una herramienta para descomposición paralela automática, donde un compilador toma un programa secuencial y genera código objeto para utilizar múltiples procesadores.
Todo sería más simple si no se necesitase la concurrencia. Pero existen muchas situaciones en las que es necesaria. Cuando disponemos de un sistema multiprocesador, realmente existen múltiples puntos de ejecución simultánea, y los hilos son un herramienta atractiva para permitir a un programa aprovecharse de este tipo de hardware. La alternativa, con la mayoría de los sistemas operativos convencionales, es configurar el programa como múltiples procesos separados, ejecutándose en un espacio de direcciones distinto. Esta opción suele ser costosa debido a que la comunicación entre los espacios de dirección es complicada y emplea numerosos recursos. Empleando un paquete de hilos, el programador puede utilizar los procesadores de forma económica. Esto parece funcionar bien en sistemas que van desde 10 procesadores hasta 1000 procesadores.
Un segundo área en el que los hilos pueden resultar útiles es en la gestión de dispositivos de E/S como discos, redes, terminales e impresoras. En estos casos un programa eficiente debe realizar otro trabajo útil mientras espera a que se complete una operación de E/S como una transferencia del disco, una recepción de un paquete de la red, etc. Esto puede ser programado fácilmente mediante hilos, de tal forma que las peticiones a un dispositivo sean secuenciales, y el hilo que realiza la petición se suspenda hasta que la solicitud es completada, mientras el programa principal realiza otro trabajo en otros hilos, solapando la ejecución de varios hilos.
Otra fuente de concurrencia son los usuarios. A veces un usuario necesita realizar dos o tres tareas simultáneamente. Los hilos son una buena forma de programar esta necesidad. La organización típica de un moderno sistema de ventanas es que cada vez que el usuario invoca una acción (presionando un botón con el ratón, por ejemplo), un hilo separado es empleado para implementar la acción. Si el usuario realiza múltiples acciones, múltiples hilos realizarán cada una de ellas en paralelo. Además normalmente se empleará un hilo exclusivo para inspeccionar las acciones del propio ratón, que es un dispositivo más.
Finalmente, otro fuente de concurrencia aparece cuando se construye un sistema distribuido. En este caso se suelen encontrar servidores de red compartidos (como un servidor de ficheros o un spooler de impresión), donde el servidor se encarga de servir peticiones de múltiples clientes. La utilización de múltiples hilos permite al servidor gestionar las solicitudes de los clientes en paralelo, en vez de procesarlas en serie o crear un proceso de servicio por cada cliente, lo cual supone un enorme gasto.
A veces puede interesar añadir concurrencia a un programa
con el objetivo de reducir la latencia de las operaciones, es
decir, el tiempo empleado entre la llamada a un procedimiento
y la finalización de dicho procedimiento. En ocasiones,
el trabajo realizado por un procedimiento puede ser diferido,
ya que no afecta al resultado del procedimiento. Por ejemplo,
cuando se añade o elimina algo en una árbol balanceado
se puede regresar de la llamada antes de volver a balancear el
árbol. Con los hilos se puede conseguir esto fácilmente :
hacer el balanceo en un hilo separado. Si el hilo separado es
planificado con una prioridad inferior, el trabajo puede realizarse
en un momento en el que el sistema esté menos ocupado (por
ejemplo, cuando se espera una entrada de usuario). Emplear hilos
para diferir el trabajo es una técnica potente, incluso
en un sistema monoprocesador. Incluso realizando el mismo trabajo,
la reducción de la latencia puede mejorar los tiempos de
respuesta de un programa.
La mayoría de los paquetes de hilos proporcionan primitivas similares para la gestión y programación con hilos, aunque a veces tienen diferencias importantes. Todos estos problemas derivados de las diferentes semánticas serán superados cuando todos los paquetes adopten el estándar POSIX. PThreads de Provenzano ha seguido esta línea, adoptando el estándar POSIX.
En general existen 3 mecanismos básicos para programar
con hilos : creación de hilos, exclusión mutua,
y espera de eventos.
1) Creación de hilos :
La primitiva de creación de hilos POSIX es pthread_create() cuya sintaxis se muestra a continuación :
int pthread_create( pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg );
A esta primitiva se le deben pasar los siguientes argumentos según el orden :
2) Exclusión mutua o mútex (mutual exclusion) :
La forma más simple de que un hilo interactúe con otros es a través de memoria compartida. En un lenguaje de alto nivel, esto es expresado mediante el acceso a variables globales. Como los hilos se ejecutan en paralelo, el programador debe planificar los accesos de forma explícita para evitar errores cuando más de un hilo trata de acceder a las variables compartidas. La herramienta más simple de hacer esto es el mútex, que es una primitiva que proporciona exclusión mutua (acceso a regiones críticas). Se trata de indicar que una región particular de código sólo puede ser ejecutada por un determinado hilo al mismo tiempo.
Un mútex debe ser primeramente creado mediante la primitiva pthread_mutex_init(), aunque primeramente la variable de mútex debe contener el valor de inicialización PTHREAD_MUTEX_INITIALIZER.
La sintaxis de la primitiva de inicialización es :
int pthread_mutex_init( pthread_mutex_t *mutex, const pthread_mutexattr_t *attr );
Un mútex tiene dos estados : bloqueado y desbloqueado. Inicialmente está desbloqueado. La primitiva pthread_mutex_lock() bloquea el mútex, y continua la ejecución. Al finalizar se debe desbloquear el mútex mediante la primitiva pthread_mutex_unlock(). Se suele decir que un hilo posee el mútex cuando se encuentra en ejecución entre las primitivas de bloqueo (lock) y desbloqueo (unlock). Si otro hilo intenta adquirir el mútex cuando ya está bloqueado, el segundo hilo se suspende (se encola a la espera del mútex) hasta que el mútex es liberado por el primer hilo. El programador puede conseguir una exclusión mutua de un conjunto de variables asociándolas con un mútex, y accediendo a las variables sólo desde el hilo que posee el mútex, es decir desde el interior del código, entre el bloqueo y desbloqueo del mútex. Esta es la idea básica de la noción de monitores descrita por Hoare.
Como ejemplo, en el siguiente fragmento de código el mútex m está asociado con la variable global cabeza. Las primitivas pthread_mutex_lock() y pthread_mutex_unlock() proporcionan exclusión mutua, lo que permite añadir, sin problemas de interferencias, la variable local nuevoElemento a la lista enlazada cuya cabeza es cabeza.
typedef struct lista { char car; lista_t *next; }lista_t ; pthread_mutex_t m; lista_t *cabeza; void Insertar_Elemento(lista_t *nuevoElemento) { pthread_mutex_lock(&m); nuevoElemento->next = cabeza; /* Sección */ cabeza = nuevoElemento; /* crítica */ pthread_mutex_unlock(&m); }
Un mútex normalmente está relacionado
con una variable global o estructura de datos global, como ocurre
en el fragmento anterior, en este caso como mucho un hilo se encuentra
ejecutando el código de la sección crítica
en un instante dado.
3) Variables de condición :
Se puede ver a un mútex como un tipo simple de mecanismo de planificación de recursos. El recurso a planificar es la memoria compartida accedida dentro de las primitivas de bloqueo y desbloqueo del mútex, y la política de planificación es un único hilo al mismo tiempo. Pero a veces el programador necesita expresar políticas de planificación más complicadas. Esto requiere la utilización de un mecanismo que permite a un hilo suspenderse hasta que suceda algún determinado evento. Este mecanismo de espera por un evento se consigue mediante una variable de condición.
Una variable de condición está siempre asociada con un mútex particular, y con los datos protegidos por el mútex. En general, un monitor consta de una serie de datos, un mútex, y cero o más variables de condición. Una variable de condición particular es siempre usada junto con el mismo mútex y sus datos.
La primitiva pthread_cond_init() permite inicializar una variable de condición para su uso posterior. La primitiva pthread_cond_wait() desbloquea el mútex y suspende el hilo (lo encola a la espera de la variable de condición) de forma atómica, en una sola acción. La primitiva pthread_cond_signal() no hace nada a menos que exista un hilo bloqueado en la variable de condición, en cuyo caso despierta a uno de los hilos bloqueado a la espera. La primitiva pthread_cond_broadcast() es similar a la anterior excepto que despierta a todos los hilos suspendidos a la espera de la variable de condición.
Cuando un hilo es despertado después de ser suspendido en la primitiva pthread_cond_wait(), vuelve a intentar adquirir el mútex, y después continua su ejecución. Si el mútex no está disponible, el hilo se suspende a la espera de que el mútex esté disponible. El mútex asociado con una variable de condición protege los datos compartidos. Si un hilo quiere un recurso, bloquea el mútex y examina los datos compartidos. Si el recurso está disponible, el hilo continua. Si no, el hilo libera el mútex y se suspende llamando a pthread_cond_wait(). Después, cuando algún hilo termina con el recurso, despierta al primer hilo mediante pthread_cond_signal() o pthread_cond_broadcast(). Por ejemplo, el siguiente fragmento de código permite que un hilo se suspenda hasta que una lista enlazada, cuya cabeza es cabeza, no esté vacía, y entonces elimina el elemento de la cabeza de la lista :
pthread_mutex_t m; pthread_cond_t condNoVacio; lista_t *cabeza; lista_t *Extraer_Elemento(void) { pthread_mutex_lock(&m); while (cabeza == NULL) /* Condición de espera */ pthread_cond_wait( &condNoVacio, &m ); elemento = cabeza; /* Sección */ cabeza = cabeza->next; /* crítica */ pthread_mutex_unlock(&m); return elemento; }
Y el siguiente fragmento de código puede ser empleado para un hilo que añade un elemento a la cabeza de la lista :
void Insertar_Elemento(lista_t *nuevoElemento) { pthread_mutex_lock(&m); nuevoElemento->next = cabeza; /* Sección */ cabeza = nuevoElemento; /* crítica */ pthread_cond_signal(&condNoVacio); /*Despertar hilos en espera */ pthread_mutex_unlock(&m); }
La regla básica para emplear exclusión mutua es sencilla : en un programa multihilo todos los datos compartidos deben estar protegidos mediante la asociación con algún mútex, y sólo se puede acceder a los datos desde un hilo que posea el mútex asociado. A continuación se presentan una serie de situaciones típicas en las que se emplean los mútex , problemas que pueden aparecer debido a su utilización y soluciones a estos problemas.
El error más simple relacionado con los mútex ocurre cuando no se protege algún dato compartido y se accede al mismo sin sincronización. Por ejemplo, supongamos el siguiente fragmento de código. La variable global tabla representa una tabla de punteros que puede ser llenada llamando a Insertar. El procedimiento trabaja insertando un argumento distinto de NULL en el índice i de tabla, y después incrementa i. La tabla está inicialmente vacía con todo a NULL.
void *tabla[1000]; int i[1001]; void InicializarTabla() { for (i = 0; i<1000; i++) tabla[i] = NULL; i = 0; } void Insertar(void *r) { if (r != NULL) { tabla[i] = r; /* (1) */ i = i+1; /* (2) */ } }
Ahora consideremos que puede suceder si un hilo A llama a Insertar(x) concurrentemente con un hilo B que llama a Insertar(y). Si el orden de ejecución resulta ser :
En este caso las acciones no consiguen el resultado esperado, ya que en vez de insertar x e y en la tabla, en índices separados, el resultado final es que y está correctamente insertado en la tabla, pero x se ha perdido. Además como (2) se ha ejecutado en dos ocasiones, la tabla ha quedado con una posición vacía. Estos errores se pueden prevenir encerrando (1) y (2) entre las primitivas de bloqueo y desbloqueo de un mútex, como se muestra a continuación :
pthread_mutex_t m; void Insertar(void *r) { if (r != NULL) { pthread_mutex_lock(&m); tabla[i] = r; /* (1) */ i = i+1; /* (2) */ pthread_mutex_unlock(&m); } }
Las primitivas de bloqueo y desbloqueo de mútex refuerzan la secuencialidad de las acciones de los hilos, de tal forma que una vez que un hilo ha ejecutado las sentencias de la sección crítica, el otro hilo puede ejecutar las suyas. Los efectos de accesos no sincronizados a los datos pueden ser muy extraños y diversos, y dependen de la relación de tiempo entre los hilos y la secuencia de ejecución del momento dado. Además en la mayoría de los entornos las relaciones de tiempo no son deterministas (porque se producen faltas de página, señales de relojes programados, o asincronismo en un sistema multiprocesador. Sería una buena idea que los lenguajes de programación proporcionasen algún método para ayudar a prevenir el acceso a variables compartidas, pero la mayoría de los lenguajes no ofrecen esta posibilidad. De todas formas los problemas del programador son menores si emplea bloqueos simples de grano grueso. Por ejemplo si se emplea un único mútex para proteger todos los datos globales de un módulo entero. Sin embargo, esto puede producir otros problemas, ya que se reducen las posibilidades de concurrencia real, ya que un bloqueo largo bloquea la ejecución de otros que esperan por el mútex. El mejor consejo es hacer que el uso de los mútex sea simple, pero no excesivamente simple.
Cuando los datos protegidos por un mútex son complicados, muchos programadores encuentran conveniente pensar en un mútex como la protección del invariante de los datos asociados. Un invariante es una función booleana de los datos, que es verdadera mientras no se posee el mútex. Así, cualquier hilo que bloquea el mútex parte inicialmente con el invariante a cierto. Cada hilo tiene la responsabilidad de restaurar el invariante antes de liberar el mútex. Esto incluye restaurar el invariante antes de llamar a pthread_cond_wait(), ya que esta primitiva libera el mútex asociado con la variable de condición.
Por ejemplo, en el fragmento de código anterior (inserción de un elemento en una tabla), el invariante es que i es el índice del primer elemento vacío (NULL) en tabla, y todos los elementos posteriores al índice i están vacíos. Las variables mencionadas en el invariante son accedidas solo cuando se posee el mútex. El invariante es falso después de la primera sentencia de asignación, pero antes de la segunda (esto sólo ocurre cuando no se posee el mútex). Normalmente los invariantes son lo suficientemente simples para que necesitemos pensar en ellos, pero a veces un programa se beneficiará si los expresamos de forma explícita. Y si son demasiado complicados de escribir, es que probablemente estemos haciendo algo mal.
La regla de que se debe usar un mútex para proteger cada acceso a las variables globales está basada en un modelo de concurrencia donde las acciones de los hilos están mezcladas arbitrariamente. Si los datos que son protegidos por un mútex son particularmente simples (por ejemplo, sólo un entero o un booleano), el programador está tentado a no usar un mútex, ya que ello introduce una considerable sobrecarga y sabe que las variables serán accedidas con operaciones atómicas y que las instrucciones no son entremezcladas. Antes de sucumbir a esta tentación, se debe considerar el tipo de hardware donde el programa se ejecutará. Si la variable entera está alineada a palabra, y si se ejecuta en un solo procesador, y si el compilador no genera variables auxiliares en registros, entonces es muy probable que se obtenga la respuesta adecuada. En otras circunstancias puede que no se obtenga la respuesta correcta, o peor aún, puede que se obtenga la respuesta correcta, pero ocasionalmente se obtenga una respuesta errónea. Obviamente no parece muy razonable tener que salvar tanto obstáculo y depender en esta medida del hardware. Para crear código correcto independiente de la máquina, se deben utilizar las técnicas de sincronización proporcionadas por el entorno de programación en el que se trabaje.
El caso más simple de bloqueo mutuo ocurre cuando un hilo intenta bloquear un mútex que ya está bloqueado. Existen más casos de deadlock que involucran a los mútex, por ejemplo :
La regla más efectiva para evitar este tipo de deadlock es aplicar un orden parcial en la adquisición de los mútex en el programa. Es decir, para cualquier par de mútex (M1, M2), cada hilo que necesite poseer M1 y M2 simultáneamente bloquea M1 y M2 en el mismo orden (por ejemplo, M1 siempre es bloqueado antes que M2). Esta regla evita los bloqueos mutuos relativos a mútex, aunque existen otro problemas de deadlock potenciales cuando se emplean variables de condición.
Existe una técnica que a veces hace fácil conseguir el orden parcial. En el ejemplo anterior, el hilo A probablemente no estaba tratando de modificar el mismo conjunto de datos que el hilo B. Frecuentemente, si examinamos el algoritmo cuidadosamente podemos dividir los datos en pequeñas partes protegidas por mútex distintos. Por ejemplo, cuando el hilo B quiere bloquear M1, puede estar esperando acceder a datos disjuntos de los que el hilo A estaba accediendo bajo M1. Es este caso podemos proteger estos datos disjuntos con un mútex separado M3, y evitar el deadlock. Esto sólo es una técnica para permitir obtener un orden parcial en los mútex (M1 antes de M2 y antes de M3, en nuestro ejemplo). Tener un programa que produce deadlock es en la mayoría de los casos un riesgo preferible a tener un programa que proporciona una respuesta errónea, pero aún así se debe intentar evitar una situación de deadlock.
Suponiendo que podemos organizar el programa de forma que pueda tener suficientes mútex para todos los datos protegidos, y una granularidad suficientemente reducida que no provoque deadlock, el resto de los problemas de los mútex son relativos al rendimiento.
Cuando un hilo posee un mútex, está potencialmente impidiendo que otro hilo progrese (si el otro hilo desea bloquear el mútex). Si el primer hilo puede usar todos los recursos de máquina, todo va bien. Pero si el primer hilo, mientras posee el mútex, deja de progresar (por ejemplo suspendiéndose a la espera de otro mútex, o debido a una falta de página, o esperando por un dispositivo de E/S), entonces la productividad total del programa decae. El problema es peor en un sistema multiprocesador, ya que un único hilo no puede utilizar toda la máquina ; en este caso la suspensión de otro hilo, puede significar que un procesador se encuentre desocupado. En general para obtener un buen rendimiento se debe considerar que los bloqueos conflictivos son eventos poco comunes. La mejor forma de reducir los bloqueos conflictivos es bloquear con un tamaño de grano fino, pero esto introduce cierto grado de complejidad. La solución es buscar un compromiso intermedio entre el tipo de granularidad y los bloqueos conflictivos.
El ejemplo más típico donde el bloqueo de granularidad es importante es en un módulo que gestiona un conjunto de objetos, por ejemplo un conjunto de ficheros abiertos. La estrategia más simple es emplear un único mútex para todas las operaciones : open, close, read, write. Pero esto impide realizar en paralelo múltiples escrituras en ficheros distintos, sin que exista una razón para ello. Una estrategia mejor es emplear un bloqueo global para las operaciones en la lista global de ficheros abiertos, y un bloqueo por fichero abierto para las operaciones que afecten sólo a dicho fichero. Esto puede conseguirse asociando un mútex con la variable que representa cada fichero abierto. El código podría parecido al siguiente :
typedef struct fichero { pthread_mutex_t mutextFichero; ... /* Otros campos relativos al fichero */ } fichero_t; pthread_mutex_t mutexGlobal; fichero_t tablaGlobal[100]; fichero_t *Open(char *nomfich) { pthread_mutex_lock(&mutexGlobal); /* Acceso a tablaGlobal */ pthread_mutex_unlock(&mutexGlobal); } void Write(fichero_t *fich) { pthread_mutex_lock(&fich->mutexFichero); /* Acceso a los campos de fich */ pthread_mutex_unlock(&fich->mutexFichero); }
En situaciones más complicadas, se pueden proteger diferentes partes de una estructura de datos con diferentes mútex. También pueden existir campos inmutables que no necesiten protección mediante mútex. En ambos casos se debe tener en cuenta la forma en que se crea la estructura de datos en memoria.
Existe una interacción entre mútex y el planificador de hilos que puede producir problemas de rendimiento muy particulares. El planificador es la parte del paquete de hilos que decide a qué hilo entre los no bloqueados se debe entregar el procesador para ejecución. Normalmente, el planificador toma esta decisión basándose en la prioridad asociada con cada hilo. Según el sistema la prioridad puede ser fija o dinámica, asignada por el programador o calculada por el planificador. A veces, el algoritmo que decide qué hilo se ejecutará no está especificado claramente. Los bloqueos conflictivos pueden llevar a una situación en la que algún hilo de prioridad elevada nunca progrese en su ejecución, a pesar de que tenga mayor prioridad que los hilos actualmente en ejecución. Este problema se conoce como inversión de prioridades. Esto puede suceder, por ejemplo, en el siguiente caso en un sistema monoprocesador :
El hilo A es de prioridad alta, el hilo B es de prioridad media y el hilo C es de baja prioridad. La secuencia de eventos es la siguiente : C está ejecutándose, porque A y B están bloqueados en algún lugar. C bloquea el mútex M. El hilo B se despierta y expulsa a C, es decir, B se ejecuta en vez de C por tener mayor prioridad. B entra en un cálculo muy largo. A se despierta y expulsa a B porque A tiene mayor prioridad. A intenta bloquear el mútex M. A se suspende porque M ya estaba bloqueado por C, y entonces el procesador se entrega a B, que continua su largo cálculo.
El efecto real es que un hilo de alta prioridad (A) es incapaz de progresar incluso aunque el procesador está siendo utilizado por un hilo de menor prioridad (B). Este estado es estable hasta que el procesador está disponible para el hilo de baja prioridad (C), que completa su trabajo y libera el mútex M. El programador puede evitar este problema indicando que C incremente su prioridad antes de bloquear M. Pero esto puede suponer otro inconveniente, ya que supone considerar que para cada mútex pueden existir otras prioridades de mútex involucradas. La solución de este problema depende de la implementación particular del paquete de hilos considerado. Sería bueno que ya que el hilo A está bloqueado en el mútex M, el hilo que posee M fuese visto por el planificador como un hilo de mayor prioridad que el propio A. Desafortunadamente, la mayoría de los paquetes de hilos no contemplan esta posibilidad.
Además, el uso de los primitivas pthread_mutex_lock() y pthread_mutex_unlock() debe ser cuidadoso, y se debe asegurar que las operaciones están correctamente emparejadas, es decir, para cada bloqueo existe su correspondiente desbloqueo al mismo nivel.
Una variable de condición es empleada cuando un programador quiere planificar la forma en que múltiples hilos acceden a algún recurso compartido, y la simple exclusión mutua proporcionada por los mútex no es suficiente.
Consideremos el siguiente ejemplo del productor/consumidor, donde uno o más hilos productores están pasando datos a uno o más consumidores. Los datos son transferidos a través de un buffer ilimitado formado por una lista enlazada cuya cabeza es la variable global cabeza. Si la lista enlazada está vacía, el consumidor se bloquea en la variable de condición NoVacio hasta que el productor genera algún dato. La lista y la variable de condición están protegidas por el mútex m.
pthread_mutex_t m; pthread_cond_t NoVacio; lista_t cabeza; lista_t *Consumidor(void) { lista_t *top; pthread_mutex_lock(&m); while (cabeza == NULL) pthread_cond_wait(&NoVacio, &m); top = cabeza; cabeza = cabeza->next; pthread_mutex_unlock(&m); return top; } void Productor(lista_t *nuevo) { pthread_mutex_lock(&m); nuevo->next = cabeza; cabeza = nuevo; pthread_cond_signal(&NoVacio); pthread_mutex_unlock(&m); }
Esto es bastante sencillo, pero todavía existen algunas sutilezas. Cuando un consumidor regresa de la llamada pthread_mutex_wait() su primera acción después de volver a bloquear el mútex es volver a comprobar si la lista enlazada está vacía. Esto es un ejemplo del siguiente patrón general, que se recomienda emplear siempre que se utilicen variables de condición :
while ( ! expresion_de_la_condición ) pthread_cond_wait(&var_condicion, &var_mutex);
Se puede pensar que volver a comprobar la expresión de la condición es redundante : en el ejemplo del productor/consumidor, el productor introduce un elemento en la lista, con lo que la lista ya no está vacía, antes de llamar a pthread_cond_signal(). Pero la semántica de pthread_cond_signal() no garantiza que el hilo despertado sea el siguiente en bloquear el mútex. Es posible que algún otro hilo consumidor interfiera, bloquee el mútex, elimine el elemento de la lista y libere el mútex, antes de que el nuevo hilo despertado pueda bloquear el mútex. Una segunda razón para volver a comprobar la condición es que se pueden despertar más de un hilo si se utilizó pthread_cond_broadcast(). Aunque la principal razón es hacer a los programas más comprensibles y más robustos. Con el bucle de la condición queda claro que la condición es cierta antes de ejecutar el resto de instrucciones.
La primitiva pthread_cond_signal() es útil si sabemos que como mucho se debe despertar un hilo. En cambio pthread_cond_broadcast() despierta todos los hilos que realizaron la llamada a pthread_cond_wait(). Si se programa empleando el bucle de comprobación de la condición después de regresar de phtread_cond_wait(), como se indicó anteriormente, entonces la corrección del programa no estará afectada si reemplazamos las llamadas pthread_cond_signal() por pthread_cond_broadcast()
La primitiva "broadcast" se emplea cuando se quiere simplificar el diseño de un programa, despertando múltiples hilos, incluso conociendo de antemano que no todos los hilo podrán progresar en su ejecución. Esto nos permite evitar la separación de diferentes condiciones de espera en diferentes variables de condición, pero este tipo de utilización implica un menor rendimiento aunque una mayor simplicidad.
Aunque el uso más habitual y correcto de broadcast es cuando realmente se necesita despertar múltiples hilos, porque el recurso que ya está disponible puede ser empleado por múltiples hilos. Un ejemplos simple donde esto es muy útil es en la política de planificación conocida como bloqueo exclusivo/compartido (o bloqueo de lectores/escritores). Comúnmente es empleado cuando tenemos datos compartidos que son leídos y escritos por varios hilos : el algoritmo será correcto y con mejor rendimiento si se permite que múltiples hilos lean los datos de forma concurrente, pero un hilo que modifique los datos debe hacerlo cuando ningún otro hilo esté accediendo a los mismos.
Los siguientes procedimientos implementan esta política de planificación. Cualquier hilo que desee leer los datos llamará a AdquirirCompartido, después leerá los datos, y llamará a LiberarCompartido. De forma similar cualquier hilo que espere modificar los datos llamará a AdquirirExclusivo, modificará los datos, y llamará a LiberarExclusivo. Cuando la variable i es mayor que cero, indica el número de lectores activos. Cuando es negativa significa que existe un escritor activo. Cuando es cero no existen hilos (ni lectores ni escritor) utilizando los datos. Si un lector potencial dentro de AdquirirCompartido observa que i es menor que cero, se suspende hasta que el escritor llama a LiberarExclusivo.
int i; pthread_mutex_t m; pthread_cond_t c; void AdquirirExclusivo() { pthread_mutex_lock(&m); /* Mientras existan lectores activos */ /* el escritor se bloquea a la espera */ while (i != 0) pthread_cond_wait(&c, &m); i = i-1 ; pthread_mutex_unlock(&m); } void AdquirirCompartido() { pthread_mutex_lock(&m); /* Mientras exista un escritor activo */ /* el lector se bloquea a la espera */ while (i < 0) pthread_cond_wait(&c, &m); i = i+1 ; pthread_mutex_unlock(&m); } void LiberarExclusivo() { pthread_mutex_lock(&m); i = 0; /* Al finalizar el escritor despierta a todos */ /* los lectores y escritores que esperan */ pthread_cond_broadcast(&c); pthread_mutex_unlock(&m); } void LiberarCompartido() { pthread_mutex_lock(&m); i = i-1; /* Si no hay más lectores en espera */ /* despierta a un escritor en espera */ if (i == 0) pthread_cond_signal(&c); pthread_mutex_unlock(&m); }
La utilización de pthread_cond_broadcast() es conveniente en LiberarExclusivo porque un escritor que finalice no necesita saber cuántos lectores están en disposición de progresar, ya que lo pueden hacer todos. Aunque se puede recodificar el ejemplo, añadiendo un contador de lectores en espera, y utilizando pthread_cond_signal() dicho número de veces en LiberarExclusivo, la función pthread_cond_broadcast() es sólo una forma más cómoda de hacer lo mismo. Obsérvese que no hay razón para emplear broadcast en LiberarCompartido, porque sabemos que sólo un hilo escritor bloqueado puede progresar. Este caso nos muestra muchos de los problemas que pueden ocurrir cuando utilizamos variables de condición.
Una vez que conocemos los conceptos formales básicos de la programación multihilo vamos a trabajar en ellos desde la perspectiva del programador.
La función pthread_create permite crear un nuevo hilo. Esta función tiene cuatro argumentos, una variable de hilo o propietario del hilo, una variable de atributos de hilo, la función inicial que será llamada cuando se inicie la ejecución del hilo, y un argumento para dicha función de comienzo. Ejemplo :
pthread_t hilo; pthread_attr_t atributos_hilo; void funcion_inicio_hilo(void *argumento); char *argumento_funcion; pthread_create( &hilo, atributos_hilo, (void *)&funcion_inicio_hilo, (void *)&argumento_funcion );
Una variable de atributos de hilo especifica una serie de atributos relativos al hilo, como puede ser el tamaño mínimo de pila para el hilo que se va a crear, aunque frecuentemente se emplean atributos por defecto usando la variable global pthread_attr_default, en la llamada de creación.
A diferencia de un proceso creado por la función
fork() de UNIX que comienza la ejecución
en el mismo punto que su padre, los hilos comienzan su ejecución
en la función especificada en la llamada de creación
pthread_create. La razón para
ello es clara, si los hilos no comienzan la ejecución en
algún punto concreto, tendríamos múltiples
hilos ejecutando las mismas instrucciones con los mismos recursos
sin conocer la situación, con los consiguientes problemas.
Recordemos que cada proceso tiene su propio conjunto de recursos
pero los hilos comparten los recursos.
Ahora que sabemos cómo crear hilos estamos preparados para realizar una primera aplicación simple. Realizaremos la aplicación más típica de todo lenguaje de programación, el programa "Hola Mundo" en su versión multihilo. La aplicación imprimirá el mensaje "Hola Mundo" en la salida estándar (stdout). El código para la versión clásica secuencial (no multihilo) de Hola Mundo es :
#include <stdio.h> void imprimir_mensaje( void *puntero ); main() { char *mensaje1 = "Hola"; char *mensaje2 = "Mundo"; imprimir_mensaje((void *)mensaje1); imprimir_mensaje((void *)mensaje2); exit(0); } void imprimir_mensaje( void *puntero ) { char *mensaje; mensaje = (char *) puntero; printf("%s ", mensaje); }
Para la versión multihilo necesitamos dos
variables de hilo y una función de comienzo para los nuevos
hilos, que será llamada cuando comiencen su ejecución.
También necesitamos alguna forma de especificar que cada
hilo debe imprimir un mensaje diferente. Una aproximación
es dividir el trabajo en cadenas de caracteres distintas y proporcionar
a cada hilo una cadena diferente como parámetro. Examinemos
el siguiente código :
#include <pthread.h> #include <stdio.h> void imprimir_mensaje( void *puntero ); main() { pthread_t hilo1, hilo2; char *mensaje1 = "Hola"; char *mensaje2 = "Mundo"; pthread_create( &hilo1, pthread_attr_default, (void*)&imprimir_mensaje, (void*) mensaje1 ); pthread_create( &hilo2, pthread_attr_default, (void*)&imprimir_mensaje, (void*) mensaje2 ); exit(0); } void imprimir_mensaje( void *puntero ) { char *mensaje; mensaje = (char *) puntero; printf("%s ", mensaje); }
Se puede observar que el programa comienza con el prototipo de la función imprimir_mensaje , y el moldeo precediendo a los argumentos mensaje1 y mensaje2 en la llamada de creación pthread_create. El programa crea el primer hilo llamando a pthread_create y pasando "Hola" como argumento inicial ; el segundo hilo es creado con "Mundo" como argumento. Cuando el primer hilo inicia la ejecución, comienza en la función imprimir_mensaje con el argumento "Hola". Imprime "Hola" y finaliza la función. Un hilo termina cuando finaliza su función inicial, así pues el primer hilo termina después de imprimir el mensaje "Hola". Cuando el segundo hilo se ejecuta, imprime "Mundo" y al igual que antes finaliza. Aunque este programa parece razonablemente correcto, posee dos defectos considerables :
Así nuestro pequeño programa Hola Mundo tiene dos condiciones de ejecución (race conditions) o condiciones problemáticas. El posibilidad de ejecución de la llamada exit y la posibilidad de qué hilo ejecutará la función printf primero. Podemos intentar arreglar estas dos condiciones de una forma artesanal. Como queremos que cada hilo hijo finalice antes que el hilo padre, podemos insertar un retraso en el hilo padre para dar tiempo a los hilos hijos a que ejecuten printf. Para asegurar que el primer hilo ejecute printf antes que el segundo, podemos insertar un retraso antes de crear el segundo hilo con la función pthread_create. El código resultante es el siguiente :
#include <pthread.h> #include <stdio.h> void imprimir_mensaje( void *puntero ); main() { pthread_t hilo1, hilo2; char *mensaje1 = "Hola"; char *mensaje2 = "Mundo"; pthread_create( &hilo1, pthread_attr_default, (void*)&imprimir_mensaje, (void*) mensaje1 ); sleep(10); pthread_create( &hilo2, pthread_attr_default, (void*)&imprimir_mensaje, (void*) mensaje2 ); sleep(10); exit(0); } void imprimir_mensaje( void *puntero ) { char *mensaje; mensaje = (char *) puntero; printf("%s ", mensaje); pthread_exit(0); }
Aunque este código parece funcionar perfectamente, no es así, pues no es seguro. Nunca es seguro confiar en los retrasos de tiempo para realizar una sincronización. Como los hilos están fuertemente acoplados, se puede tener la tentación de emplear una forma menos rigurosa que la sincronización, pero esta tentación debe ser evitada. La condición problemática aquí es exactamente la misma que la que tenemos con una aplicación distribuida y un recurso compartido. El recurso es la salida estándar y los elementos de la computación distribuida son los tres hilos. El hilo hilo1 debe usar la salida estándar antes de hilo2 y ambos deben realizar sus acciones antes de que el hilo padre realice la llamada exit.
Además de nuestros intentos por sincronizar
mediante retrasos, hemos cometido otro error. La función
sleep, al igual que la función
exit, es relativa a procesos. Cuando un hilo ejecuta sleep
el proceso entero duerme, es decir, todos los hilos duermen cuando
el proceso duerme. Así, nos encontramos en la misma situación
que teníamos sin las llamadas a sleep,
y además el programa tarda 20 segundos más en su
ejecución.
La versión correcta de Hola Mundo empleando primitivas de sincronización que evitan las condiciones de ejecución se presenta en el siguiente código :
#include <pthread.h> #include <stdio.h> typedef struct param { char *mensaje; pthread_t hilo; } param_t; void imprimir_mensaje( void *puntero ); main() { pthread_t hilo1, hilo2; param_t mensaje1 = {"Hola", 0}; param_t mensaje2 = {"Mundo", 0}; pthread_create( &hilo1, pthread_attr_default, (void*)&imprimir_mensaje, (void*) mensaje1 ); mensaje2.hilo = hilo1; pthread_create( &hilo2, pthread_attr_default, (void*)&imprimir_mensaje, (void*) mensaje2 ); pthread_join( hilo2, NULL ) exit(0); } void imprimir_mensaje( void *puntero ) { param_t *datos; datos = (paramt_t *) puntero; if (datos->hilo != 0) pthread_join( datos->hilo, NULL ); printf("%s ", datos->mensaje); pthread_exit( 0 ); }
En esta versión se puede observar que el parámetro de la función imprimir_mensaje es del tipo definido por el usuario param_t, que es una estructura que nos permite pasar el mensaje y el identificador de un hilo. El hilo padre tras crear el hilo1, obtiene su identificador y lo almacena como parámetro para el hilo2. Ambos hilos ejecutan la función inicial imprimir_mensaje, pero la ejecución es distinta para cada hilo. El primer hilo contendrá 0 en el campo hilo de la estructura datos con lo que no ejecutará la función pthread_join, ejecutará printf y ejecutará la función pthread_exit. El segundo hilo, en cambio, contendrá el identificador del primer hilo en el campo hilo de la estructura datos, y ejecutará la función pthread_join, que lo que hace es esperar a la finalización del primer hilo. Después ejecuta la impresión con printf, y después realiza la finalización con pthread_exit. El hilo padre, tras la creación de los dos hilos se sincroniza con la finalización del hilo2 mediante pthread_join.
El paquete PThreads proporciona dos primitivas de sincronización básicas : los mútex, y las variables de condición.
Un mútex es una primitiva de bloqueo simple que puede ser usada para controlar el acceso a un recurso compartido. En los hilos, todo el espacio de direcciones es compartido, y de esta forma puede ser considerado como un recurso compartido. Normalmente, en la mayoría de los casos los hilos trabajan individualmente con variables locales (conceptualmente), que son aquellas creadas por la función de inicio del hilo y las funciones llamadas desde ella, y combinan sus esfuerzos mediante variables globales a todos los hilos. El acceso a los recursos de escritura que sean compartidos debe estar controlado.
Veamos un ejemplo simple del problema de lectores/escritores donde un lector y un escritor se comunican usando un buffer compartido, y el acceso es controlado mediante un mútex.
void funcion_lector(void); /* Lector */ void funcion_escritor(void); /* Escritor */ char buffer; /* Buffer compartido */ int n_elementos_buffer = 0; /* No. de elementos en el buffer */ pthread_mutex_t mutex; /* Mutex de acceso al buffer */ main() { pthread_t lector; /* Hilo lector */ pthread_mutex_init(&mutex, pthread_mutexattr_default); /* Creamos el hilo lector, y el hilo padre será el escritor */ pthread_create( &lector, pthread_attr_default, (void*)&funcion_lector, NULL); funcion_escritor(); } void funcion_escritor(void) { while(1) { pthread_mutex_lock( &mutex ); if ( n_elementos_buffer == 0 ) { buffer = hacer_nuevo_elemento(); buffer_has_item = 1; } pthread_mutex_unlock( &mutex ); /* Entregamos la CPU a otro hilo */ /* para dar la posibilidad de que se ejecute el lector */ pthread_yield(); } } void funcion_lector(void) { while(1) { pthread_mutex_lock( &mutex ); if ( n_elementos_buffer == 1) { consumir_elemento( buffer ); n_elementos_buffer = 0; } pthread_mutex_unlock( &mutex ); /* Entregamos la CPU a otro hilo */ /* para dar la posibilidad de que se ejecute el lector */ pthread_yield(); } }
En este simple programa se supone que el buffer sólo puede contener un elemento, con lo cual sólo se puede encontrar en dos situaciones, tener un elemento o no tenerlo. El escritor primero solicita el bloqueo del mútex, suspendiéndose hasta que el mútex es liberado si ya estaba bloqueado. El mútex se inicializó con atributos por defecto
pthread_mutexattr_default. Después comprueba si el buffer está vacío. Si lo está, crea un nuevo elemento y habilita el flag n_elementos_buffer, así el lector sabrá que el buffer contiene un elemento. Luego desbloquea el mútex y se retrasa durante unos segundos para dar la oportunidad al lector de consumir el elemento.
El lector realiza acciones similares. Obtiene el
bloqueo del mútex, comprueba si existe un elemento en el
buffer, y si es así consume dicho elemento. Libera el bloqueo
del mútex y se retrasa durante un corto periodo de tiempo
para dar una oportunidad al escritor de crear un nuevo elemento.
En este ejemplo, el escritor (productor) y el lector (consumidor)
se ejecutan indefinidamente, produciendo y consumiendo elementos.
Normalmente si un mútex ya no es necesario en un programa
se destruye mediante pthread_mutex_destroy(&mutex).
La utilización de mútex permite evitar
las condiciones de ejecución (race conditions). Normalmente
la primitiva mútex por sí sola es muy débil,
ya que sólo tiene dos estados, bloqueado o desbloqueado.
POSIX proporciona otro elemento, la variable de condición,
que permite que un hilo se bloquee a la espera de una señal
de otro hilo (espera por una condición). Cuando se recibe
la señal, el hilo bloqueado es despertado e intenta obtener
el bloqueo del mútex relacionado. Así las señales
y los mútex pueden ser combinados para eliminar el problema
de bloqueo circular que presenta el problema de los lectores/escritores.
A continuación vamos a explicar los pasos
que se deben seguir para realizar una aplicación multihilo
mediante la librería Pthreads que proporciona este paquete,
una vez instalado el paquete.
En primer lugar hay que plantear el problema desde el punto de vista de la programación concurrente, y utilizar todas las herramientas (mútex, variables de condición, datos específicos de hilo, etc.) para sincronización y gestión de hilos junto con sus respectivas funciones de manejo, que el paquete Pthreads nos ofrece. Para poder emplear todas estas características debemos incluir el fichero de cabecera pthread.h mediante la directiva : #include <pthread.h> al principio de nuestro fuente.
Para escribir el programa podemos emplear nuestro editor de textos favorito. Una vez escrito el ejemplo, debemos compilarlo empleando el script que acompaña al paquete : pgcc o pg++, según el tipo de código que empleemos en nuestra aplicación., de la misma forma que se emplea el compilador gcc o g++. Estos scripts están especialmente preparados para compilar los fuentes que empleen el paquete Pthreads, ya que incluyen la librería libpthread.a durante la etapa de enlace. Un ejemplos sería : pgcc miprograma.c
Se debe tener cuidado con la inicialización de las variables relativas a mútex, variables de condición y ejecución una sola vez. Todas las variables de este tipo se deben inicializar antes de ser empleadas. Para ello existen dos opciones :
Ambas opciones se pueden emplear de forma conjunta sin ningún problema.
Un aspecto importante que se debe tener en cuenta cuando se escriban aplicaciones con Pthreads es que no se deben utilizar funciones POSIX que no se implementan en el paquete, como es el caso de las funciones de cancelación pthread_cancel y pthread_setcancel y algunas otras. Recordemos que el paquete Pthreads no implementa completamente el estándar POSIX 1003.1c.
Salvo estas consideraciones particulares y puntuales, y el cambio de mentalidad que supone pensar en concurrencia, la programación de una aplicación multihilo mediante el paquete Pthreads no supone mayores problemas y basta seguir el estándar POSIX.
A continuación vamos a dar una serie de recomendaciones que se deben seguir a la hora de programar una aplicación multihilo, con el objetivo de que la programación sea más sencilla, clara y fácil de depurar, intentando evitar los problemas que plantea la programación concurrente y el modelo de hilos.
Listado de funciones por categorías.
A continuación se proporciona el listado organizado
por categorías de las principales funciones de la librería
Pthreads con su sintaxis. Para una información detallada
de cada función se debe consultar el Manual del Programador.
Este grupo de funciones permite la creación, destrucción, inicialización y gestión de los hilos de la aplicación.
Las estructuras de datos y funciones relacionadas se encuentran declaradas en el archivo de cabecera pthread.h
Las funciones básicas de gestión de hilos son :
void pthread_init( void ); int pthread_create( pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg ); int pthread_equal( pthread_t thread_1, pthread_t thread_2 ); int pthread_exit( void *status ); int pthread_join( pthread_t thread, void **status ); pthread_t pthread_self( void ); int pthread_getschedparam(pthread_t thread, int *policy, struct sched_param *param); int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param); int pthread_detach( pthread_t ); void pthread_yield( void ); int pthread_kill(struct pthread *thread, int sig); int pthread_signal(int sig, void (*dispatch)(int));
Este grupo de funciones permite la inicialización, destrucción, bloqueo y liberalización de los mútex de la aplicación.
Las estructuras de datos y funciones relacionadas se encuentran declaradas en el archivo de cabecera mutex.h
Los tipos básicos definidos son :
typedef struct pthread_mutex { enum pthread_mutextype m_type; struct pthread_queue m_queue; struct pthread * m_owner; semaphore m_lock; union pthread_mutex_data m_data; long m_flags; } pthread_mutex_t; typedef struct pthread_mutexattr { enum pthread_mutextype m_type; long m_flags; } pthread_mutexattr_t;
Las funciones de gestión de mútex son :
int pthread_mutex_init( pthread_mutex_t *mutex, const pthread_mutexattr_t *attr ); int pthread_mutex_lock( pthread_mutex_t *mutex ); int pthread_mutex_unlock( pthread_mutex_t *mutex ); int pthread_mutex_trylock( pthread_mutex_t *mutex ); int pthread_mutex_destroy( pthread_mutex_t *mutex );
Este grupo de funciones permite la inicialización, destrucción, espera y señalización de las variables de condición de la aplicación.
Las estructuras de datos y funciones relacionadas se encuentran declaradas en el archivo de cabecera cond.h
Los tipos básicos definidos son :
typedef struct pthread_cond { enum pthread_condtype c_type; struct pthread_queue c_queue; semaphore c_lock; void * c_data; long c_flags; } pthread_cond_t; typedef struct pthread_condattr { enum pthread_condtype c_type; long c_flags; } pthread_condattr_t;
Las funciones de gestión de variables de condición son :
int pthread_cond_init( pthread_cond_t *cond, const pthread_condattr_t *attr ); int pthread_cond_timedwait( pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime ); int pthread_cond_wait( pthread_cond_t *cond, pthread_mutex_t *mutex ); int pthread_cond_signal( pthread_cond_t *cond ); int pthread_cond_broadcast( pthread_cond_t *cond ); int pthread_cond_destroy( pthread_cond_t *cond );
Este grupo de funciones permite la inicialización, destrucción, establecimiento y obtención de atributos de las variables de atributos de hilo de la aplicación.
Las estructuras de datos y funciones relacionadas se encuentran declaradas en el archivo de cabecera pthread_attr.h.
Los tipos básicos definidos son :
enum schedparam_policy { SCHED_RR, SCHED_IO, SCHED_FIFO, SCHED_OTHER }; struct pthread_attr { enum schedparam_policy schedparam_policy; int prio; int flags; void * arg_attr; void (*cleanup_attr)(); void * stackaddr_attr; size_t stacksize_attr; };
Las funciones de gestión de los atributos de hilo son :
int pthread_attr_init(pthread_attr_t *attr ); int pthread_attr_destroy(pthread_attr_t *attr ); int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize ); int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize ); int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr); int pthread_attr_getstackaddr(pthread_attr_t *attr, void **stackaddr); int pthread_attr_setdetachstate(const pthread_attr_t *attr, int state ); int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *state ); int pthread_attr_setscope(pthread_attr_t *attr, int contentionscope); int pthread_attr_getscope(pthread_attr_t *attr, int *contentionscope) int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched); int pthread_attr_getinheritsched(pthread_attr_t *attr, int *inheritsched); int pthread_attr_setschedpolicy(pthread_attr_t *attr, int schedpolicy) int pthread_attr_getschedpolicy(pthread_attr_t *attr, int *schedpolicy) int pthread_attr_setschedparam(pthread_attr_t *attr, struct sched_param param) int pthread_attr_getschedparam(pthread_attr_t *attr, struct sched_param *param); int pthread_attr_setfloatstate(pthread_attr_t *attr, int floatstate) int pthread_attr_getfloatstate(pthread_attr_t *attr, int *floatstate) int pthread_attr_setcleanup(pthread_attr_t *attr, void (*routine)(void *), void * arg)
Este grupo de funciones permite la inicialización, destrucción, establecimiento y obtención de atributos de las variables de atributos de mútex de la aplicación.
Las estructuras de datos y funciones relacionadas se encuentran declaradas en el archivo fuente mutexattr.c
Las funciones de gestión de atributos de mútex son :
int pthread_mutexattr_init( pthread_mutexattr_t *attr ); int pthread_mutexattr_gettype(pthread_mutexattr_t *attr, unsigned int *type); int pthread_mutexattr_settype(pthread_mutexattr_t *attr, unsigned int type) int pthread_mutexattr_destroy( pthread_mutexattr_t *attr );
Este grupo de funciones permite la inicialización, destrucción, establecimiento y obtención de atributos de las variables de condición de la aplicación.
Las estructuras de datos y funciones relacionadas se encuentran declaradas en el archivo fuente condattr.c
Las funciones de gestión de atributos de variable de condición son :
int pthread_condattr_init(pthread_condattr_t *attr); int pthread_condattr_gettype(pthread_mutexattr_t *attr, unsigned int *type); int pthread_condattr_settype(pthread_condattr_t *attr, unsigned int type); int pthread_condattr_destroy(pthread_condattr_t *attr);
Este grupo de funciones permite la introducción y extracción de funciones en la pila de gestores de limpieza.
Las estructuras de datos y funciones relacionadas se encuentran declaradas en el archivo de cabecera cleanup.h
Los tipos básicos definidos son :
struct pthread_cleanup { struct pthread_cleanup *next; void (*routine)(); void *routine_arg; };
Las funciones de gestión de la pila son :
void pthread_cleanup_push( void (*routine)(void *), void *routine_arg ); void pthread_cleanup_pop( int execute );
Este grupo de funciones permite la creación, destrucción, establecimiento y obtención de datos específicos de hilo.
Las estructuras de datos y funciones relacionadas se encuentran declaradas en el archivo de cabecera specific.h
Los tipos básicos definidos son :
struct pthread_key { pthread_mutex_t mutex; long count; void (*destructor)(); }; typedef int pthread_key_t;
Las funciones de gestión de datos específicos de hilo son :
int pthread_key_create( pthread_key_t *key, void (*dest_routine)(void *) ); int pthread_setspecific( pthread_key_t key, const void *pointer ); void *pthread_getspecific( pthread_key_t key ); int pthread_key_delete( pthread_key_t key );
Este grupo de funciones permite la gestión de las colas de prioridades que empleará el planificador de hilos del paquete.
Las estructuras de datos y funciones relacionadas se encuentran declaradas en el archivo de cabecera prio_queue.h
Los tipos básicos definidos son :
struct pthread_prio_level { struct pthread * first; struct pthread * last; }; struct pthread_prio_queue { void * data; struct pthread * next; struct pthread_prio_level level[PTHREAD_MAX_PRIORITY + 1]; };
Las funciones de gestión de las colas de prioridad son :
void pthread_prio_queue_init(struct pthread_prio_queue *queue); void pthread_prio_queue_enq(struct pthread_prio_queue *queue, struct pthread *thread); struct pthread *pthread_prio_queue_deq( struct pthread_prio_queue *queue );
Este grupo de funciones permite la gestión de las colas de hilos que empleará el paquete de hilos para su gestión.
Las estructuras de datos y funciones relacionadas se encuentran declaradas en el archivo de cabecera queue.h
Los tipos básicos definidos son :
struct pthread_queue { struct pthread *q_next; struct pthread *q_last; void *q_data; };
Las funciones de gestión de las colas de hilos son :
void pthread_queue_init(struct pthread_queue *queue); void pthread_queue_enq(struct pthread_queue *queue, struct pthread *thread); int pthread_queue_remove(struct pthread_queue *queue, struct pthread *thread); struct pthread *pthread_queue_get(struct pthread_queue *queue); struct pthread *pthread_queue_deq(struct pthread_queue *queue);
Este grupo de funciones permite la gestión de la máscara de señales y las señales de los hilos de la aplicación.
Las funciones relacionadas se encuentran declaradas
en el archivo de cabecera pthread.h
o bien no se encuentran en un fichero de cabecera sino en un fichero
fuente :
int pthread_sigmask(int how, const sigset_t *set, sigset_t * oset); int pthread_signal(int sig, void (*dispatch)(int)); int sigwait(const sigset_t *set, int *sig); void sleep_schedule(struct timespec *current_time, struct timespec *new_time)
Este grupo de funciones permite la gestión de las funciones de ejecución una sola vez de los hilos de la aplicación.
Las estructuras de datos y funciones relacionadas se encuentran declaradas en el archivo de cabecera pthread_once.h
Los tipos básicos definidos son :
typedef struct pthread_once { int state; pthread_mutex_t mutex; } pthread_once_t;
La función de gestión de ejecución una sola vez es :
pthread_once_t once_init = PTHREAD_ONCE_INIT; int pthread_once( pthread_once_t *once_init, void (*init_routine)(void) );
Este grupo de funciones permite la gestión de la E/S de ficheros y bloqueos.
Las funciones relacionadas se encuentran declaradas en el archivo de cabecera stdio.h
Las funciones especiales de E/S son las siguientes :
void flockfile( FILE *file ); int ftrylockfile( FILE *file ); void funlockfile( FILE *file ); int getc_unlocked( FILE *file ); int getchar_unlocked( void ); int putc_unlocked( int c, FILE *file ); int putchar_unlocked( int c );
También existen versiones multihilo (hilo-seguras)
de numerosas funciones de gestión de E/S, otras de la librería
estándar y otras funciones de uso habitual.
Para obtener un información detallada de cada
una de las funciones se debe consultar el Manual del Programador.
El paquete de hilos MIT Pthreads de Chris Provenzano es un paquete de nivel usuario. El rendimiento del paquete está, pues, limitado a la utilización de un solo procesador, pero si puede servir como plataforma de desarrollo de aplicaciones que empleen el estándar POSIX 1003.1c , de forma que la aplicación desarrollada sea transportable a otro sistema multiprocesador con soporte de hilos a nivel kernel.
Junto con el paquete, en su versión 1.60 beta 5, se entregan una serie de programas de prueba y una serie de programas "benchmark" para medir el rendimiento del paquete en el sistema concreto.
Los programas de benchmark que se entregan se pueden encontrar en el directorio tests del paquete, y son los siguientes :
Como muestra del rendimiento se ofrecen algunos resultados obtenidos de la ejecución de estos benchmarks en un sistema Intel 486-66Mhz con 9Mb Ram bajo el sistema operativo Linux 1.2.13, con la versión 1.60 beta 5, y carga mínima en el sistema :
Benchmark | Operación | Tiempo (en microsg.) |
p_bench_pthread_create | 10.000 hilos creados | 2.278.534 |
p_bench_mutex | 1.000.000 bloqueos/desbloqueos | 33.703.650 |
p_bench_yield | 10 hilos con 10.000 cambios de contexto. | 4.664.460 |
p_bench_read | 100.000 operaciones de lectura | 21.757.734 |
p_bench_getpid | 1.000.000 llamadas getpid() | 17.690.668 |
p_bench_semaphore | 1.000..000 operaciones TST+CLEAR | 4.334.479 |
bench_fcntl | 100.000 llamadas fcntl() | 19.061.241 |
bench_pipe | 1.000 pipes | 1.363.568 |
bench_read | 100.000 operaciones de lectura | 20.277.983 |
Aunque estos datos dependen del sistema concreto,
si se puede observar, por ejemplo, la velocidad de creación
de los hilos, la velocidad en el bloqueo y desbloqueo de un mútex,
o la velocidad en el cambio de contexto de hilos. Esto demuestra
que los hilos son muy rápidos en determinadas operaciones.