← Concurrencia y Sincronización Bloque 7 · Problemas clásicos de sincronización · Tema 32 de 47

Tema 32

pthread_rwlock e inanición

POSIX proporciona pthread_rwlock, un candado de lectura y escritura del sistema. Es más conveniente que implementarlo a mano, pero tiene un costo real y puede causar inanición de escritores. En este tema verás la API completa, el costo real del rwlock, cómo los escritores pueden quedar esperando para siempre, y cómo evitarlo.

1. Qué es pthread_rwlock

Un read-write lock (candado de lectura y escritura) es un candado que permite que múltiples lectores accedan simultáneamente al recurso, pero requiere acceso exclusivo para los escritores. pthread_rwlock es la implementación de POSIX.

El rwlock es una optimización del mutex para el caso de muchos lectores y pocos escritores: en lugar de serializar todos los accesos (como un mutex), permite que los lectores se ejecuten en paralelo.

La diferencia con un mutex es que el rwlock tiene dos tipos de adquisición: rdlock (lectura) y wrlock (escritura). Múltiples rdlock pueden coexistir; wrlock es exclusivo.

2. La API de pthread_rwlock

FunciónDescripciónRetorno
pthread_rwlock_init(rwlock, attr)Inicializa el rwlock.0 si OK, código de error si no.
pthread_rwlock_destroy(rwlock)Destruye el rwlock.0 si OK, código de error si no.
pthread_rwlock_rdlock(rwlock)Adquiere el lock para lectura.0 si OK, código de error si no.
pthread_rwlock_wrlock(rwlock)Adquiere el lock para escritura (exclusivo).0 si OK, código de error si no.
pthread_rwlock_unlock(rwlock)Libera el lock.0 si OK, código de error si no.
pthread_rwlock_tryrdlock(rwlock)Intenta adquirir para lectura sin bloquear.0 si OK, EBUSY si está ocupado.
pthread_rwlock_trywrlock(rwlock)Intenta adquirir para escritura sin bloquear.0 si OK, EBUSY si está ocupado.
pthread_rwlock_timedrdlock(rwlock, abstime)Intenta adquirir para lectura con timeout.0 si OK, ETIMEDOUT si timeout.
pthread_rwlock_timedwrlock(rwlock, abstime)Intenta adquirir para escritura con timeout.0 si OK, ETIMEDOUT si timeout.

3. Inicialización y destrucción

inicialización
#include <pthread.h>

/* Forma 1: inicialización estática. */
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

/* Forma 2: inicialización dinámica. */
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);

/* Destruir cuando ya no se use. */
pthread_rwlock_destroy(&rwlock);

La inicialización estática es la más común. La dinámica se usa cuando el rwlock se crea en tiempo de ejecución.

4. rdlock y wrlock

pthread_rwlock_rdlock adquiere el lock para lectura. Varios hilos pueden adquirir el rdlock simultáneamente. pthread_rwlock_wrlock adquiere el lock para escritura: es exclusivo, ningún otro hilo (ni lector ni escritor) puede adquirir el lock.

rdlock y wrlock
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

/* Lector. */
pthread_rwlock_rdlock(&rwlock);
/* leer el recurso */
pthread_rwlock_unlock(&rwlock);

/* Escritor. */
pthread_rwlock_wrlock(&rwlock);
/* escribir el recurso */
pthread_rwlock_unlock(&rwlock);

La regla es: rdlock es compartido, wrlock es exclusivo. Múltiples rdlock pueden coexistir; wrlock no puede coexistir con ningún otro lock.

5. unlock

pthread_rwlock_unlock libera el lock, sin importar si se adquirió con rdlock o wrlock. El mismo unlock se usa para ambos.

Al liberar un rdlock, si hay escritores esperando, uno de ellos puede adquirir el lock (dependiendo de la implementación). Al liberar un wrlock, los lectores o escritores esperando pueden adquirir el lock.

6. tryrdlock, trywrlock, timedrdlock, timedwrlock

Las variantes no bloqueantes y con timeout funcionan igual que en los mutexes:

try y timed
/* Intentar adquirir para lectura sin bloquear. */
int ret = pthread_rwlock_tryrdlock(&rwlock);
if (ret == 0) {
    /* Se adquirió. */
    pthread_rwlock_unlock(&rwlock);
} else if (ret == EBUSY) {
    /* El lock está ocupado (por un escritor). */
}

/* Intentar adquirir para escritura con timeout. */
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 5;
ret = pthread_rwlock_timedwrlock(&rwlock, &ts);
if (ret == ETIMEDOUT) {
    /* Timeout. */
}

7. Un ejemplo desarrollado

Un ejemplo de uso de rwlock para proteger una estructura de datos compartida:

rwlock_ejemplo.c
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

typedef struct {
    int datos[100];
    int num_datos;
    pthread_rwlock_t rwlock;
} recurso_t;

void recurso_init(recurso_t *r) {
    r->num_datos = 0;
    pthread_rwlock_init(&r->rwlock, NULL);
}

void recurso_leer(recurso_t *r, int *buf, int *n) {
    pthread_rwlock_rdlock(&r->rwlock);
    *n = r->num_datos;
    for (int i = 0; i < r->num_datos; i++) {
        buf[i] = r->datos[i];
    }
    pthread_rwlock_unlock(&r->rwlock);
}

void recurso_escribir(recurso_t *r, int valor) {
    pthread_rwlock_wrlock(&r->rwlock);
    r->datos[r->num_datos++] = valor;
    pthread_rwlock_unlock(&r->rwlock);
}

void recurso_destroy(recurso_t *r) {
    pthread_rwlock_destroy(&r->rwlock);
}

Los lectores pueden leer simultáneamente (rdlock), pero los escritores necesitan exclusión (wrlock).

8. El costo real

El rwlock tiene un costo mayor que un mutex, incluso cuando no hay contención:

OperaciónMutexRWLockNotas
Adquirir (sin contención)~20 ns~50 nsEl rwlock tiene más estado que mantener.
Liberar (sin contención)~20 ns~50 nsEl rwlock debe verificar si hay escritores esperando.
Adquirir (con contención)~1-10 μs~1-10 μsAmbos bloquean en el núcleo.
Memoria~40 bytes~56 bytesEl rwlock tiene más campos.

El rwlock es más costoso que un mutex porque mantiene más estado (contadores de lectores, colas de escritores, etc.). Si las lecturas son muy cortas, el overhead del rwlock puede ser mayor que el beneficio del paralelismo.

La regla práctica es: usar rwlock solo cuando las lecturas son largas y frecuentes, y las escrituras son raras. Si las lecturas son cortas, un mutex es mejor.

El rwlock no siempre es mejor

El rwlock es más costoso que un mutex. Si las lecturas son cortas (microsegundos), el overhead del rwlock puede ser mayor que el beneficio del paralelismo. Usar rwlock solo cuando las lecturas son largas y frecuentes.

9. La inanición de escritores

La inanición de escritores ocurre cuando un escritor espera indefinidamente porque siempre hay lectores. Si llegan lectores continuamente, el escritor nunca puede adquirir el lock exclusivo.

Este es el problema principal del rwlock: si hay un flujo continuo de lectores, los escritores sufren inanición. Esto es análogo a la inanición en la planificación por prioridades (tema 18).

La inanición de escritores es un problema grave en sistemas con muchas lecturas y pocas escrituras: los escritores (que pueden ser críticos, como actualizar una configuración) nunca se ejecutan.

10. Por qué ocurre la inanición

La inanición ocurre porque los lectores tienen prioridad sobre los escritores en la mayoría de las implementaciones:

  1. Un escritor intenta adquirir el lock. Se bloquea porque hay lectores. bloquear
  2. Llegan más lectores. Se les permite adquirir el lock porque los lectores tienen prioridad. prioridad
  3. El escritor sigue esperando. Cada vez que un lector libera el lock, otro lector lo adquiere antes de que el escritor pueda. esperar
  4. Inanición: el escritor espera indefinidamente. inanición

El problema es que los lectores tienen prioridad: si siempre hay lectores, el escritor nunca obtiene el lock.

11. Soluciones a la inanición

Hay varias soluciones a la inanición de escritores:

SoluciónCómo funcionaVentajaDesventaja
Prioridad a escritoresCuando un escritor está esperando, los nuevos lectores se bloquean.Los escritores no sufren inanición.Los lectores pueden sufrir inanición.
Cola FIFOLos hilos se atienden en orden de llegada, sin prioridad a lectores.Equitativo.Menos paralelismo de lectura.
TimeoutEl escritor espera un tiempo máximo; si no obtiene el lock, toma una acción alternativa.Evita espera indefinida.No resuelve la causa.
Escritor preferenteSi hay un escritor esperando, se le da prioridad sobre los nuevos lectores, pero los lectores actuales siguen.Buen compromiso.Más complejo.

La solución más común es la prioridad a escritores: cuando un escritor está esperando, los nuevos lectores se bloquean. Los lectores actuales siguen ejecutándose, pero cuando terminan, el escritor obtiene el lock antes de que lleguen nuevos lectores.

12. Cómo se implementa en glibc

La implementación de pthread_rwlock en glibc usa una estructura con:

La implementación por defecto da prioridad a los lectores. Se puede cambiar el comportamiento con atributos (aunque no todos los sistemas lo soportan).

13. Simulador interactivo

Este simulador muestra la inanición de escritores y cómo la prioridad a escritores la resuelve:

Simulador · inanición de escritores

14. Resumen

15. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué es un read-write lock?

Un candado que permite múltiples lectores simultáneos y escritores exclusivos.

2. ¿Qué es rdlock?

Adquirir el lock para lectura (compartido).

3. ¿Qué es wrlock?

Adquirir el lock para escritura (exclusivo).

4. ¿Cuál es el costo del rwlock vs. mutex?

El rwlock es ~2-3× más costoso que un mutex.

5. ¿Qué es la inanición de escritores?

Un escritor espera indefinidamente porque siempre hay lectores.

6. ¿Por qué ocurre la inanición?

Porque los lectores tienen prioridad sobre los escritores.

7. ¿Cómo se resuelve la inanición?

Con prioridad a escritores: los nuevos lectores se bloquean si hay un escritor esperando.

8. ¿Cuándo conviene usar rwlock?

Cuando las lecturas son largas y frecuentes, y las escrituras son raras.

9. ¿Cuándo NO conviene usar rwlock?

Cuando las lecturas son cortas (el overhead del rwlock es mayor que el beneficio).

10. ¿Cómo se implementa el rwlock en glibc?

Con contadores, colas y variables de condición internas.

Para practicar

  1. Implementa un programa con pthread_rwlock y observa el comportamiento.
  2. Usa tryrdlock y trywrlock y observa el retorno EBUSY.
  3. Implementa la prioridad a escritores y verifica que evita la inanición.
  4. Mide el costo del rwlock vs. mutex con perf.
  5. Explica por qué el rwlock no siempre es mejor que un mutex.
  6. Investiga cómo glibc implementa pthread_rwlock.