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:
- Datos compartidos: las variables que se protegen.
- Un mutex: para la exclusión mutua.
- Variables de condición: para esperar a que se cumplan condiciones sobre los datos.
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.
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:
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:
- Los datos son privados: solo se acceden a través de las funciones del monitor.
- El mutex se bloquea al entrar y se libera al salir: en todas las funciones.
- Las variables de condición se usan con el mutex: para esperar a que se cumpla una condición.
- La condición se verifica en un while: para manejar los despertares espurios.
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:
- Dato: el saldo.
- Mutex: para la exclusión mutua.
- Variable de condición: para esperar a que haya suficiente saldo.
- Métodos: depositar, retirar, saldo.
4. Código de la cuenta bancaria
#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:
- Datos: el buffer, el frente, el fondo, el tamaño, la capacidad.
- Mutex: para la exclusión mutua.
- Variables de condición: una para «no vacío» y otra para «no lleno».
- Métodos: poner, quitar, tamaño.
5. Código de la cola acotada
#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
| Aspecto | Cuenta bancaria | Cola acotada |
|---|---|---|
| Datos | Un entero (saldo). | Un array circular (buffer). |
| Condición | saldo >= monto. | tamaño > 0 (consumir), tamaño < capacidad (producir). |
| Variables de condición | 1 (suficiente). | 2 (no_vacio, no_lleno). |
| Métodos | depositar, retirar, saldo. | poner, quitar, tamaño. |
| Patrón | Esperar 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
- Encapsulación: los datos y la sincronización están en la misma estructura.
- Reutilización: el monitor se puede usar en cualquier programa.
- Thread-safety: el monitor es thread-safe por construcción.
- Claridad: el código es más legible que con mutex sueltos.
8. Desventajas
- Verboso: más código que con mutex sueltos.
- Sin soporte del lenguaje: el compilador no verifica que se use correctamente.
- Menos flexible: no se puede usar trylock fácilmente.
- Gestión de memoria: hay que inicializar y destruir manualmente.
9. Simulador interactivo
Este simulador muestra cómo funciona la cola acotada (monitor) con productores y consumidores:
Simulador · cola acotada (monitor)
| Paso | Hilo | Operación | Buffer | Estado |
|---|
10. Resumen
- Un monitor en C es una estructura con datos, mutex y variables de condición.
- Las funciones del monitor bloquean el mutex, operan y desbloquean.
- La cuenta bancaria usa una condición (saldo suficiente).
- La cola acotada usa dos condiciones (no vacío, no lleno).
- La condición se verifica en un while.
- Los monitores en C son verbosos pero claros y reutilizables.
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
- Implementa la cuenta bancaria y verifica que el saldo es consistente.
- Implementa la cola acotada y úsala en un productor-consumidor.
- Añade un método "transferir" a la cuenta bancaria.
- Implementa un monitor para una pila acotada.
- Usa ThreadSanitizer para verificar que no hay carreras.
- Investiga cómo se implementan los monitores en el núcleo de Linux.