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ón | Descripción | Retorno |
|---|---|---|
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
#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.
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:
/* 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:
#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ón | Mutex | RWLock | Notas |
|---|---|---|---|
| Adquirir (sin contención) | ~20 ns | ~50 ns | El rwlock tiene más estado que mantener. |
| Liberar (sin contención) | ~20 ns | ~50 ns | El rwlock debe verificar si hay escritores esperando. |
| Adquirir (con contención) | ~1-10 μs | ~1-10 μs | Ambos bloquean en el núcleo. |
| Memoria | ~40 bytes | ~56 bytes | El 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 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:
- Un escritor intenta adquirir el lock. Se bloquea porque hay lectores. bloquear
- Llegan más lectores. Se les permite adquirir el lock porque los lectores tienen prioridad. prioridad
- El escritor sigue esperando. Cada vez que un lector libera el lock, otro lector lo adquiere antes de que el escritor pueda. esperar
- 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ón | Cómo funciona | Ventaja | Desventaja |
|---|---|---|---|
| Prioridad a escritores | Cuando un escritor está esperando, los nuevos lectores se bloquean. | Los escritores no sufren inanición. | Los lectores pueden sufrir inanición. |
| Cola FIFO | Los hilos se atienden en orden de llegada, sin prioridad a lectores. | Equitativo. | Menos paralelismo de lectura. |
| Timeout | El escritor espera un tiempo máximo; si no obtiene el lock, toma una acción alternativa. | Evita espera indefinida. | No resuelve la causa. |
| Escritor preferente | Si 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:
- Contador de lectores: cuántos lectores tienen el lock.
- Flag de escritor: si un escritor tiene el lock.
- Cola de escritores: los escritores esperando.
- Cola de lectores: los lectores esperando.
- Mutex interno: para proteger la estructura.
- Variables de condición: para bloquear y despertar.
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
- pthread_rwlock permite múltiples lectores simultáneos y escritores exclusivos.
- rdlock es compartido, wrlock es exclusivo.
- El rwlock es más costoso que un mutex (más estado, más overhead).
- La inanición de escritores ocurre cuando siempre hay lectores.
- La prioridad a escritores resuelve la inanición.
- El rwlock conviene cuando las lecturas son largas y frecuentes.
- Linux implementa el rwlock con contadores y colas.
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
- Implementa un programa con pthread_rwlock y observa el comportamiento.
- Usa tryrdlock y trywrlock y observa el retorno EBUSY.
- Implementa la prioridad a escritores y verifica que evita la inanición.
- Mide el costo del rwlock vs. mutex con perf.
- Explica por qué el rwlock no siempre es mejor que un mutex.
- Investiga cómo glibc implementa pthread_rwlock.