← Concurrencia y Sincronización Bloque 6 · Variables de condición y monitores · Tema 28 de 47

Tema 28

Implementar un monitor en C

C no tiene monitores nativos, pero se pueden implementar con mutex y variables de condición. En este tema verás cómo construir un monitor en C, con dos ejemplos: una cuenta bancaria (depósitos, retiros, consulta de saldo) y una cola acotada (productor-consumidor). Ambos encapsulan los datos y la sincronización en una estructura que es thread-safe por construcción.

1. Qué es implementar un monitor en C

En Java o C#, un monitor es una construcción del lenguaje: el compilador genera automáticamente el código de sincronización. En C, no hay monitores nativos, pero se pueden implementar manualmente usando mutex y variables de condición.

Un monitor en C es una estructura (struct) que contiene:

Las operaciones del monitor son funciones que reciben un puntero a la estructura, bloquean el mutex, realizan la operación y desbloquean el mutex. La exclusión mutua es implícita: el programador no maneja el mutex directamente.

Monitor en C = struct + mutex + cond

En C, un monitor es una estructura que contiene los datos y los mecanismos de sincronización. Las funciones que operan sobre la estructura son los métodos del monitor. La exclusión mutua es implícita porque las funciones bloquean y desbloquean el mutex automáticamente.

2. El patrón: estructura + mutex + condición

El patrón para implementar un monitor en C es:

patrón de monitor en C
typedef struct {
    /* Datos compartidos. */
    int dato;

    /* Mecanismos de sincronización. */
    pthread_mutex_t mutex;
    pthread_cond_t cond;
} monitor_t;

void monitor_init(monitor_t *m) {
    m->dato = 0;
    pthread_mutex_init(&m->mutex, NULL);
    pthread_cond_init(&m->cond, NULL);
}

void monitor_operacion(monitor_t *m, int valor) {
    pthread_mutex_lock(&m->mutex);
    /* ... operación sobre los datos ... */
    /* ... verificar condición ... */
    /* ... pthread_cond_wait/broadcast ... */
    pthread_mutex_unlock(&m->mutex);
}

void monitor_destroy(monitor_t *m) {
    pthread_mutex_destroy(&m->mutex);
    pthread_cond_destroy(&m->cond);
}

Las reglas del patrón son:

3. Monitor 1: cuenta bancaria

El primer monitor es una cuenta bancaria con depósitos, retiros y consulta de saldo. La condición es: un retiro solo se puede hacer si hay suficiente saldo.

El monitor tiene:

4. Código de la cuenta bancaria

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

typedef struct {
    int saldo;
    pthread_mutex_t mutex;
    pthread_cond_t cond_suficiente;
} cuenta_t;

void cuenta_init(cuenta_t *c, int saldo_inicial) {
    c->saldo = saldo_inicial;
    pthread_mutex_init(&c->mutex, NULL);
    pthread_cond_init(&c->cond_suficiente, NULL);
}

void cuenta_depositar(cuenta_t *c, int monto) {
    pthread_mutex_lock(&c->mutex);
    c->saldo += monto;
    pthread_cond_broadcast(&c->cond_suficiente);  /* despertar a los que esperan */
    pthread_mutex_unlock(&c->mutex);
}

int cuenta_retirar(cuenta_t *c, int monto) {
    pthread_mutex_lock(&c->mutex);
    while (c->saldo < monto) {
        pthread_cond_wait(&c->cond_suficiente, &c->mutex);
    }
    c->saldo -= monto;
    pthread_mutex_unlock(&c->mutex);
    return 1;  /* éxito */
}

int cuenta_saldo(cuenta_t *c) {
    int s;
    pthread_mutex_lock(&c->mutex);
    s = c->saldo;
    pthread_mutex_unlock(&c->mutex);
    return s;
}

void cuenta_destroy(cuenta_t *c) {
    pthread_mutex_destroy(&c->mutex);
    pthread_cond_destroy(&c->cond_suficiente);
}

El método cuenta_retirar espera a que haya suficiente saldo usando pthread_cond_wait. Cuando otro hilo deposita, hace pthread_cond_broadcast para despertar a los hilos que esperan.

La condición se verifica en un while (no if) para manejar los despertares espurios.

4. Monitor 2: cola acotada

El segundo monitor es una cola acotada (bounded buffer) para el problema productor-consumidor. La cola tiene una capacidad máxima, y los productores esperan si la cola está llena, mientras que los consumidores esperan si la cola está vacía.

El monitor tiene:

5. Código de la cola acotada

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

#define CAPACIDAD 10

typedef struct {
    int buffer[CAPACIDAD];
    int frente;
    int fondo;
    int tamano;
    pthread_mutex_t mutex;
    pthread_cond_t no_vacio;   /* hay datos para consumir */
    pthread_cond_t no_lleno;   /* hay espacio para producir */
} cola_t;

void cola_init(cola_t *c) {
    c->frente = 0;
    c->fondo = 0;
    c->tamano = 0;
    pthread_mutex_init(&c->mutex, NULL);
    pthread_cond_init(&c->no_vacio, NULL);
    pthread_cond_init(&c->no_lleno, NULL);
}

void cola_poner(cola_t *c, int valor) {
    pthread_mutex_lock(&c->mutex);
    while (c->tamano == CAPACIDAD) {
        pthread_cond_wait(&c->no_lleno, &c->mutex);  /* esperar espacio */
    }
    c->buffer[c->fondo] = valor;
    c->fondo = (c->fondo + 1) % CAPACIDAD;
    c->tamano++;
    pthread_cond_signal(&c->no_vacio);  /* señalar que hay datos */
    pthread_mutex_unlock(&c->mutex);
}

int cola_quitar(cola_t *c) {
    pthread_mutex_lock(&c->mutex);
    while (c->tamano == 0) {
        pthread_cond_wait(&c->no_vacio, &c->mutex);  /* esperar datos */
    }
    int valor = c->buffer[c->frente];
    c->frente = (c->frente + 1) % CAPACIDAD;
    c->tamano--;
    pthread_cond_signal(&c->no_lleno);  /* señalar que hay espacio */
    pthread_mutex_unlock(&c->mutex);
    return valor;
}

int cola_tamano(cola_t *c) {
    int t;
    pthread_mutex_lock(&c->mutex);
    t = c->tamano;
    pthread_mutex_unlock(&c->mutex);
    return t;
}

void cola_destroy(cola_t *c) {
    pthread_mutex_destroy(&c->mutex);
    pthread_cond_destroy(&c->no_vacio);
    pthread_cond_destroy(&c->no_lleno);
}

La cola acotada usa dos variables de condición: no_vacio (para los consumidores) y no_lleno (para los productores). Cada una tiene su propia condición y su propia señal.

6. Comparación de los dos monitores

AspectoCuenta bancariaCola acotada
DatosUn entero (saldo).Un array circular (buffer).
Condiciónsaldo >= monto.tamaño > 0 (consumir), tamaño < capacidad (producir).
Variables de condición1 (suficiente).2 (no_vacio, no_lleno).
Métodosdepositar, retirar, saldo.poner, quitar, tamaño.
PatrónEsperar a que haya suficiente.Productor-consumidor.

Ambos monitores siguen el mismo patrón: encapsular datos y sincronización en una estructura, con métodos que bloquean el mutex, verifican la condición en un while, y señalan a los hilos que esperan.

7. Ventajas de implementar monitores en C

8. Desventajas

9. Simulador interactivo

Este simulador muestra cómo funciona la cola acotada (monitor) con productores y consumidores:

Simulador · cola acotada (monitor)

PasoHiloOperaciónBufferEstado

10. Resumen

11. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué es un monitor en C?

Una estructura con datos compartidos, un mutex y variables de condición.

2. ¿Qué patrón siguen los monitores en C?

Estructura + mutex + condición. Las funciones bloquean, operan y desbloquean.

3. ¿Qué condición usa la cuenta bancaria?

saldo >= monto (suficiente saldo para retirar).

4. ¿Qué condiciones usa la cola acotada?

tamaño > 0 (no vacío) y tamaño < capacidad (no lleno).

5. ¿Por qué la cola acotada usa dos variables de condición?

Una para los consumidores (no vacío) y otra para los productores (no lleno).

6. ¿Por qué se usa while y no if?

Para manejar los despertares espurios.

7. ¿Qué ventaja tiene el monitor sobre el mutex suelto?

Encapsulación: los datos y la sincronización están en la misma estructura.

8. ¿Qué desventaja tiene el monitor en C?

Es verboso y no tiene soporte del lenguaje.

9. ¿Cómo se destruye un monitor?

Destruyendo el mutex y las variables de condición.

10. ¿Se puede usar trylock en un monitor?

Sí, pero no es común porque el monitor encapsula la sincronización.

Para practicar

  1. Implementa la cuenta bancaria y verifica que el saldo es consistente.
  2. Implementa la cola acotada y úsala en un productor-consumidor.
  3. Añade un método "transferir" a la cuenta bancaria.
  4. Implementa un monitor para una pila acotada.
  5. Usa ThreadSanitizer para verificar que no hay carreras.
  6. Investiga cómo se implementan los monitores en el núcleo de Linux.