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

Tema 31

Lectores y escritores

El problema de lectores y escritores modela una situación común: muchos hilos leen un dato compartido, y ocasionalmente un hilo lo modifica. Varios lectores pueden leer simultáneamente, pero un escritor necesita acceso exclusivo. En este tema verás las variantes del problema (que favorecen a lectores o a escritores), las soluciones con semáforos y variables de condición, y el riesgo de inanición.

1. El problema

El problema de lectores y escritores modela un recurso compartido (como una base de datos, un archivo o una estructura de datos) al que acceden dos tipos de procesos: lectores que solo leen el recurso, y escritores que lo modifican. Varios lectores pueden leer simultáneamente, pero un escritor necesita acceso exclusivo (ningún otro lector ni escritor puede estar accediendo).

El problema fue propuesto por Courtois, Heymans y Parnas en 1971. Es uno de los problemas clásicos de sincronización porque modela situaciones reales (bases de datos, cachés, configuraciones) donde las lecturas son frecuentes y las escrituras son raras.

Las restricciones son:

2. Las variantes

Hay tres variantes del problema, que difieren en la prioridad que se da a lectores vs. escritores:

VariantePrioridadVentajaDesventaja
PrimeraLectoresMáximo paralelismo de lectura.Los escritores pueden sufrir inanición.
SegundaEscritoresLos escritores no esperan indefinidamente.Los lectores pueden sufrir inanición.
TerceraEquitativaNadie sufre inanición.Menos paralelismo que la primera.

3. Primera solución: prioridad a lectores

En la primera variante, si hay lectores esperando, se les da prioridad sobre los escritores. Los escritores solo se ejecutan cuando no hay lectores.

La solución usa un mutex para proteger el contador de lectores y un semáforo para la exclusión del escritor:

lectores prioritarios
int num_lectores = 0;
sem_t mutex;       /* protege num_lectores */
sem_t recurso;     /* exclusión del escritor (valor 1) */

void lector(void) {
    sem_wait(&mutex);
    num_lectores++;
    if (num_lectores == 1) {
        sem_wait(&recurso);  /* primer lector bloquea al escritor */
    }
    sem_post(&mutex);

    /* Leer el recurso. */

    sem_wait(&mutex);
    num_lectores--;
    if (num_lectores == 0) {
        sem_post(&recurso);  /* último lector libera al escritor */
    }
    sem_post(&mutex);
}

void escritor(void) {
    sem_wait(&recurso);  /* esperar a que no haya lectores */
    /* Escribir el recurso. */
    sem_post(&recurso);
}

El problema de esta solución es que si llegan lectores continuamente, el escritor espera indefinidamente (inanición del escritor).

4. Segunda solución: prioridad a escritores

En la segunda variante, si hay un escritor esperando, se le da prioridad sobre los lectores. Los lectores que llegan después del escritor deben esperar a que el escritor termine.

La solución usa un semáforo adicional para bloquear a los lectores cuando hay un escritor esperando:

escritores prioritarios
int num_lectores = 0;
int num_escritores_esperando = 0;
sem_t mutex;          /* protege los contadores */
sem_t recurso;        /* exclusión (valor 1) */
sem_t no_escritor;    /* bloquea lectores si hay escritor esperando */

void lector(void) {
    sem_wait(&no_escritor);  /* esperar si hay escritor */
    sem_wait(&mutex);
    num_lectores++;
    if (num_lectores == 1) {
        sem_wait(&recurso);
    }
    sem_post(&mutex);
    sem_post(&no_escritor);

    /* Leer el recurso. */

    sem_wait(&mutex);
    num_lectores--;
    if (num_lectores == 0) {
        sem_post(&recurso);
    }
    sem_post(&mutex);
}

void escritor(void) {
    sem_wait(&mutex);
    num_escritores_esperando++;
    sem_post(&mutex);

    sem_wait(&recurso);  /* esperar a que no haya lectores */
    /* Escribir el recurso. */
    sem_post(&recurso);

    sem_wait(&mutex);
    num_escritores_esperando--;
    sem_post(&mutex);
}

El problema de esta solución es que si llegan escritores continuamente, los lectores esperan indefinidamente (inanición del lector).

5. Tercera solución: equitativa

En la tercera variante, se usa una cola FIFO para garantizar que nadie espera indefinidamente. Los lectores y escritores se turnan en orden de llegada.

Esta solución es más compleja pero garantiza la equidad. Se implementa con una cola de espera y un semáforo para cada tipo de acceso.

6. Código completo

La primera variante (prioridad a lectores) es la más usada en la práctica:

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

int recurso = 0;
int num_lectores = 0;
sem_t mutex;
sem_t recurso_sem;

void *lector(void *arg) {
    int id = *(int *)arg;
    for (int i = 0; i < 3; i++) {
        sem_wait(&mutex);
        num_lectores++;
        if (num_lectores == 1) {
            sem_wait(&recurso_sem);
        }
        sem_post(&mutex);

        /* Leer el recurso. */
        printf("Lector %d: lee %d\n", id, recurso);
        usleep(100000);

        sem_wait(&mutex);
        num_lectores--;
        if (num_lectores == 0) {
            sem_post(&recurso_sem);
        }
        sem_post(&mutex);
    }
    return NULL;
}

void *escritor(void *arg) {
    int id = *(int *)arg;
    for (int i = 0; i < 2; i++) {
        sem_wait(&recurso_sem);
        /* Escribir el recurso. */
        recurso++;
        printf("Escritor %d: escribe %d\n", id, recurso);
        usleep(200000);
        sem_post(&recurso_sem);
    }
    return NULL;
}

int main(void) {
    sem_init(&mutex, 0, 1);
    sem_init(&recurso_sem, 0, 1);

    pthread_t lectores[4];
    pthread_t escritores[2];
    int ids_l[4] = {0, 1, 2, 3};
    int ids_e[2] = {0, 1};

    for (int i = 0; i < 4; i++) pthread_create(&lectores[i], NULL, lector, &ids_l[i]);
    for (int i = 0; i < 2; i++) pthread_create(&escritores[i], NULL, escritor, &ids_e[i]);

    for (int i = 0; i < 4; i++) pthread_join(lectores[i], NULL);
    for (int i = 0; i < 2; i++) pthread_join(escritores[i], NULL);

    sem_destroy(&mutex);
    sem_destroy(&recurso_sem);
    return 0;
}

7. Propiedades

PropiedadPrimera (lectores)Segunda (escritores)Tercera (equitativa)
Varios lectores simultáneosSíSíSí
Escritor exclusivoSíSíSí
Inanición de lectoresNoSí (posible)No
Inanición de escritoresSí (posible)NoNo
ComplejidadBajaMediaAlta

8. Inanición

La inanición es el principal problema de las variantes con prioridad:

La solución equitativa evita la inanición usando una cola FIFO, pero es más compleja de implementar.

La inanición es el trade-off de la prioridad

Cualquier variante que favorezca a un tipo de proceso puede causar inanición en el otro. La única forma de evitar la inanición es dar turnos equitativos (FIFO).

9. Simulador interactivo

Este simulador muestra cómo lectores y escritores acceden a un recurso compartido:

Simulador · lectores y escritores

10. Resumen

11. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué es el problema de lectores y escritores?

Un recurso compartido con lectores (lectura simultánea) y escritores (acceso exclusivo).

2. ¿Cuántos lectores pueden leer simultáneamente?

Cualquier número, si no hay escritor.

3. ¿Cuántos escritores pueden escribir simultáneamente?

Solo uno (acceso exclusivo).

4. ¿Qué semáforos se usan en la primera variante?

mutex (protege el contador) y recurso (exclusión del escritor).

5. ¿Qué ocurre con la inanición en la primera variante?

Los escritores pueden sufrir inanición si llegan lectores continuamente.

6. ¿Qué ocurre con la inanición en la segunda variante?

Los lectores pueden sufrir inanición si llegan escritores continuamente.

7. ¿Cómo evita la inanición la tercera variante?

Usando una cola FIFO para dar turnos equitativos.

8. ¿Por qué se necesita el semáforo mutex?

Para proteger el contador de lectores.

9. ¿Qué ocurre cuando el último lector sale?

Libera el recurso (sem_post(&recurso)) para que un escritor pueda entrar.

10. ¿Qué ocurre cuando un escritor entra?

Adquiere el recurso (sem_wait(&recurso)), bloqueando a lectores y escritores.

Para practicar

  1. Implementa la primera variante y observa la inanición de escritores.
  2. Implementa la segunda variante y observa la inanición de lectores.
  3. Implementa la tercera variante (equitativa) con una cola FIFO.
  4. Usa pthread_rwlock y compara con tu implementación.
  5. Explica por qué los lectores no necesitan exclusión mutua entre sí.
  6. Investiga cómo se implementa pthread_rwlock en glibc.