Utilización de exclusiones mutuas
Un mútex es un bloqueo de exclusión mutua. Sólo una hebra puede retener el bloqueo.
Los exclusiones mutuas se utilizan para proteger los datos u otros recursos del acceso simultáneo. Un mutex tiene atributos, que especifican las características del mutex.
Objeto de atributos de mútex
Al igual que los hilos, los mutexes se crean con la ayuda de un objeto de atributos. El objeto de atributos mutex es un objeto abstracto que contiene varios atributos, en función de la implementación de las opciones POSIX . Se accede a él a través de una variable de tipo pthread_mutexattr_t. En AIX®, el tipo de datos pthread_mutexattr_t es un puntero; en otros sistemas, puede ser una estructura u otro tipo de datos.
Creación y destrucción del objeto de atributos mutex
La subrutina pthread_mutexattr_init inicializa el objeto de atributos de mútex con los valores predeterminados. Los atributos se manejan mediante subrutinas. La subrutina pthread_mutexattr_destroy destruye el objeto de atributos de hebra. Esta subrutina puede liberar almacenamiento asignado dinámicamente por la subrutina pthread_mutexattr_init , en función de la implementación de la biblioteca de hebras.
pthread_mutexattr_t attributes;
/* the attributes object is created */
...
if (!pthread_mutexattr_init(&attributes)) {
/* the attributes object is initialized */
...
/* using the attributes object */
...
pthread_mutexattr_destroy(&attributes);
/* the attributes object is destroyed */
}Se puede utilizar el mismo objeto de atributos para crear varios exclusiones mutuas. También se puede modificar entre creaciones de mutex. Cuando se crean los mutexes, el objeto de atributos se puede destruir sin afectar a los mutexes creados con él.
Atributos de mútex
Se definen los siguientes atributos de mútex:
| Atributo | Descripción |
|---|---|
| Protocolo | Especifica el protocolo utilizado para evitar inversiones de prioridad para un mútex. Este atributo depende de la herencia de prioridad o de la opción POSIX de protección de prioridad. |
| Proceso-compartido | Especifica el proceso de compartición de un mútex. Este atributo depende de la opción POSIX de compartición de procesos. |
Para obtener más información sobre estos atributos, consulte Opciones de biblioteca de hebras y Planificación de sincronización.
Creación y destrucción de exclusiones mutuas
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
...
pthread_mutexattr_init(&attr);
pthread_mutex_init(&mutex, &attr);
pthread_mutexattr_destroy(&attr);pthread_mutex_t mutex;
...
pthread_mutex_init(&mutex, NULL);El ID del mútex creado se devuelve a la hebra de llamada a través del parámetro mutex . El ID de mútex es un objeto opaco; su tipo es pthread_mutex_t. En AIX, el tipo de datos pthread_mutex_t es una estructura; en otros sistemas, puede ser un puntero u otro tipo de datos.
Se debe crear un mutex una vez. Sin embargo, evite llamar a la subrutina pthread_mutex_init más de una vez con el mismo parámetro mutex (por ejemplo, en dos hebras que ejecutan simultáneamente el mismo código). Garantizar la exclusividad de una creación de mútex se puede hacer de las siguientes maneras:
- Llamando a la subrutina pthread_mutex_init antes de la creación de otras hebras que utilizarán este mútex; por ejemplo, en la hebra inicial.
- Llamando a la subrutina pthread_mutex_init dentro de una rutina de inicialización de una sola vez. Para obtener más información, consulte Inicializaciones de un solo uso.
- Utilizando un mútex estático inicializado por la macro de inicialización estática PTHREAD_MUTEX_INITIALIZER ; el mútex tendrá atributos predeterminados.
pthread_mutex_t mutex;
...
for (i = 0; i < 10; i++) {
/* creates a mutex */
pthread_mutex_init(&mutex, NULL);
/* uses the mutex */
/* destroys the mutex */
pthread_mutex_destroy(&mutex);
}Al igual que cualquier recurso del sistema que se pueda compartir entre hebras, se debe destruir un mútex asignado en la pila de una hebra antes de que se termine la hebra. La biblioteca de hebras mantiene una lista enlazada de exclusiones mutuas. Por lo tanto, si la pila donde se asigna un mutex se libera, la lista se corromperá.
Tipos de exclusiones mutuas
- PTHREAD_MUTEX_DEFAULT o PTHREAD_MUTEX_NORMAL
- Da como resultado un punto muerto si el mismo pthread intenta bloquearlo una segunda vez utilizando la subrutina pthread_mutex_lock sin desbloquearlo primero. Este es el tipo por omisión.
- PTHREAD_MUTEX_ERRORCHECK
- Evita los puntos muertos devolviendo un valor distinto de cero si la misma hebra intenta bloquear el mismo mutex más de una vez sin desbloquear primero el mutex.
- PTHREAD_MUTEX_RECURSIVE
- Permite que el mismo pthread bloquee de forma recursiva el mutex utilizando la subrutina pthread_mutex_lock sin que se produzca un punto muerto ni se obtenga un valor de retorno distinto de cero de pthread_mutex_lock. El mismo pthread tiene que llamar a la subrutina pthread_mutex_unlock el mismo número de veces que ha llamado a la subrutina pthread_mutex_lock para desbloquear el mutex para que lo utilicen otros pthreads.
Cuando se crea por primera vez un atributo de mútex, tiene un tipo predeterminado de PTHREAD_MUTEX_NORMAL. Después de crear el mútex, el tipo se puede cambiar utilizando la llamada de biblioteca de API pthread_mutexattr_settype .
pthread_mutexattr_t attr;
pthread_mutex_t mutex;
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex, &attr);
struct {
int a;
int b;
int c;
} A;
f()
{
pthread_mutex_lock(&mutex);
A.a++;
g();
A.c = 0;
pthread_mutex_unlock(&mutex);
}
g()
{
pthread_mutex_lock(&mutex);
A.b += A.a;
pthread_mutex_unlock(&mutex);
}Bloqueo y desbloqueo de exclusiones mutuas
Un mutex es un bloqueo simple, que tiene dos estados: bloqueado y desbloqueado. Cuando se crea, se desbloquea un mútex. La subrutina pthread_mutex_lock bloquea el mútex especificado en las condiciones siguientes:
- Si el mútex está desbloqueado, la subrutina lo bloquea.
- Si el mutex ya está bloqueado por otra hebra, la subrutina bloquea la hebra de llamada hasta que se desbloquea el mutex.
- Si el mutex ya está bloqueado por la hebra de llamada, la subrutina puede bloquear para siempre o devolver un error en función del tipo de mutex.
La subrutina pthread_mutex_trylock actúa como la subrutina pthread_mutex_lock sin bloquear la hebra de llamada en las condiciones siguientes:
- Si el mútex está desbloqueado, la subrutina lo bloquea.
- Si el mútex ya está bloqueado por cualquier hebra, la subrutina devuelve un error.
La hebra que ha bloqueado un mútex a menudo se denomina propietario del mútex.
La subrutina pthread_mutex_unlock restablece el mutex especificado al estado desbloqueado si es propiedad del mutex de llamada en las condiciones siguientes:
- Si el mútex ya estaba desbloqueado, la subrutina devuelve un error.
- Si el mutex era propiedad de la hebra de llamada, la subrutina desbloquea el mutex.
- Si el mutex era propiedad de otra hebra, la subrutina podría devolver un error o desbloquear el mutex en función del tipo de mutex. No se recomienda desbloquear el mutex porque los mutexes suelen estar bloqueados y desbloqueados por el mismo pthread.
Debido a que el bloqueo no proporciona un punto de cancelación, no se puede cancelar una hebra bloqueada mientras se espera un mútex. Por lo tanto, se recomienda que utilice exclusiones mutuas sólo durante breves periodos de tiempo, como en las instancias en las que está protegiendo los datos del acceso simultáneo. Para obtener más información, consulte Puntos de cancelación y Cancelación de una hebra.
Protección de datos con exclusiones mutuas
Los mutexes están pensados para servir como primitivos de bajo nivel a partir de los cuales se pueden crear otras funciones de sincronización de hebras o como un bloqueo de protección de datos. Para obtener más información sobre cómo implementar bloqueos largos y bloqueos de lectores/grabadores de prioridad de grabador, consulte Utilización de exclusiones mutuas.
Ejemplo de uso de mútex
/* the initial thread */
pthread_mutex_t mutex;
int i;
...
pthread_mutex_init(&mutex, NULL); /* creates the mutex */
for (i = 0; i < num_req; i++) /* loop to create threads */
pthread_create(th + i, NULL, rtn, &mutex);
... /* waits end of session */
pthread_mutex_destroy(&mutex); /* destroys the mutex */
...
/* the request handling thread */
... /* waits for a request */
pthread_mutex_lock(&db_mutex); /* locks the database */
... /* handles the request */
pthread_mutex_unlock(&db_mutex); /* unlocks the database */
...La hebra inicial crea el mútex y todas las hebras de manejo de solicitudes. El mutex se pasa a la hebra utilizando el parámetro de la rutina de punto de entrada de la hebra. En un programa real, la dirección del mutex puede ser un campo de una estructura de datos más compleja pasada al hilo creado.
Cómo evitar puntos muertos
- Un mútex creado con el tipo predeterminado, PTHREAD_MUTEX_NORMAL, no puede ser rebloqueado por el mismo pthread sin que se produzca un punto muerto.
- Una aplicación puede estar en punto muerto al bloquear exclusiones mutuas en orden inverso. Por ejemplo, el siguiente fragmento de código puede producir un punto muerto entre las hebras A y B.
/* Thread A */ pthread_mutex_lock(&mutex1); pthread_mutex_lock(&mutex2); /* Thread B */ pthread_mutex_lock(&mutex2); pthread_mutex_lock(&mutex1); - Una aplicación puede estar en punto muerto en lo que se denomina punto muerto de recurso . Por ejemplo:
Esta aplicación tiene dos hebras,struct { pthread_mutex_t mutex; char *buf; } A; struct { pthread_mutex_t mutex; char *buf; } B; struct { pthread_mutex_t mutex; char *buf; } C; use_all_buffers() { pthread_mutex_lock(&A.mutex); /* use buffer A */ pthread_mutex_lock(&B.mutex); /* use buffers B */ pthread_mutex_lock(&C.mutex); /* use buffer C */ /* All done */ pthread_mutex_unlock(&C.mutex); pthread_mutex_unlock(&B.mutex); pthread_mutex_unlock(&A.mutex); } use_buffer_a() { pthread_mutex_lock(&A.mutex); /* use buffer A */ pthread_mutex_unlock(&A.mutex); } functionB() { pthread_mutex_lock(&B.mutex); /* use buffer B */ if (..some condition) { use_buffer_a(); } pthread_mutex_unlock(&B.mutex); } /* Thread A */ use_all_buffers(); /* Thread B */ functionB();thread Aythread B.Thread Bempieza a ejecutarse primero y, a continuación,thread Ase inicia poco después. Sithread Aejecuta use_all_buffers () y bloquea correctamente A.mutex, se bloqueará cuando intente bloquear B.mutex, porquethread Bya lo ha bloqueado. Mientrasthread Bejecuta functionB ysome_conditionse produce mientrasthread Aestá bloqueado,thread Btambién bloqueará el intento de adquirir A.mutex, que ya está bloqueado porthread A. Esto da como resultado un punto muerto.La solución a este punto muerto es que cada hebra adquiera todos los bloqueos de recursos que necesita antes de utilizar los recursos. Si no puede adquirir los bloqueos, debe liberarlos y volver a empezar.
Mutexes y condiciones de carrera
Los bloqueos de exclusión mutua (mutexes) pueden evitar incoherencias de datos debidas a condiciones de carrera. A menudo se produce una condición de actualización cuando dos o más hebras deben realizar operaciones en la misma área de memoria, pero los resultados de los cálculos dependen del orden en el que se realizan estas operaciones.
move X, REG
inc REG
move REG, XSi ambas hebras del ejemplo anterior se ejecutan simultáneamente en dos CPU, o si la planificación hace que las hebras se ejecuten de forma alternativa en cada instrucción, pueden producirse los pasos siguientes:
- La hebra A ejecuta la primera instrucción y coloca X, que es 1, en el registro de la hebra A. A continuación, la hebra B se ejecuta y coloca X, que es 1, en el registro de la hebra B. El ejemplo siguiente ilustra los registros resultantes y el contenido de la memoria X.
Thread A Register = 1 Thread B Register = 1 Memory X = 1 - La hebra A ejecuta la segunda instrucción e incrementa el contenido de su registro a 2. A continuación, la hebra B incrementa su registro a 2. No se mueve nada a la memoria X, por lo que la memoria X permanece igual. El ejemplo siguiente ilustra los registros resultantes y el contenido de la memoria X.
Thread A Register = 2 Thread B Register = 2 Memory X = 1 - La hebra A mueve el contenido de su registro, que ahora es 2, a la memoria X. A continuación, la hebra B mueve el contenido de su registro, que también es 2, a la memoria X, sobrescribiendo el valor de la hebra A. El ejemplo siguiente ilustra los registros resultantes y el contenido de la memoria X.
Thread A Register = 2 Thread B Register = 2 Memory X = 2
En la mayoría de los casos, la hebra A y la hebra B ejecutan las tres instrucciones una detrás de la otra, y el resultado sería 3, como se esperaba. Las condiciones de carrera suelen ser difíciles de descubrir, porque ocurren de forma intermitente.
Para evitar esta condición de carrera, cada hebra debe bloquear los datos antes de acceder al contador y actualizar la memoria X. Por ejemplo, si la hebra A toma un bloqueo y actualiza el contador, deja la memoria X con un valor de 2. Después de que la hebra A libere el bloqueo, la hebra B toma el bloqueo y actualiza el contador, tomando 2 como su valor inicial para X y lo incrementa a 3, el resultado esperado.