Ya tienes las herramientas: mutex, semáforos, variables de condición, bloqueos de lectura/escritura y barreras. Pero saber qué hace cada primitiva no es lo mismo que saber combinarlas. Un mutex protege un dato; coordinar dos flujos que dependen el uno del otro requiere componer varias primitivas en un orden concreto, y ahí es donde la intuición falla y aparecen los cuelgues.
Por suerte no hay que inventar nada. Entre 1965 y 1971, Dijkstra, Courtois, Hoare y otros identificaron un puñado de problemas que destilan las dificultades esenciales de la coordinación, y desde entonces son el vocabulario común de la disciplina. Cuando un ingeniero dice "esto es un productor-consumidor", cuatro compañeros entienden en dos segundos la estructura del código, dónde está el riesgo y qué solución probar. Cuando dice "aquí tenemos inanición de escritores", saben exactamente qué medir.
Además —y esto es lo que hace que valga la pena estudiarlos— casi todo problema real es una variante de uno de ellos: una cola de trabajos es productor-consumidor, una caché consultada y refrescada es lectores-escritores, un grupo de hilos esperando peticiones es el barbero dormilón, y un servicio que toma dos candados es una versión de los filósofos. Esta lección desarrolla los cuatro, cada uno con su planteamiento, sus soluciones ingenuas y por qué fallan, la solución correcta comentada línea a línea, y su traducción directa a una pieza de Meteora. Terminamos con una tabla que enlaza cada patrón real con su problema clásico.
Contenido
- Por qué estos problemas son el lenguaje común
- Productor-consumidor con búfer limitado
- La solución con tres semáforos, paso a paso
- La variante con mutex y variable de condición
- La pérdida de señal y el orden de los
wait - Lectores-escritores: el planteamiento
- Prioridad a lectores y la inanición de escritores
- Prioridad a escritores y qué hace
rwlockde verdad - Filósofos comensales
- El barbero dormilón
- Qué patrón real corresponde a cada problema
Por qué estos problemas son el lenguaje común
Los cuatro problemas no son ejercicios académicos: cada uno aísla una dificultad distinta que no aparece en los demás.
| Problema | Dificultad que aísla | Pregunta que responde |
|---|---|---|
| Productor-consumidor | Coordinar ritmos distintos con recursos finitos | ¿Cómo espero a que haya sitio o haya datos? |
| Lectores-escritores | Acceso asimétrico al mismo recurso | ¿Cómo dejo pasar a muchos y luego a uno solo, sin que nadie pase hambre? |
| Filósofos comensales | Adquirir varios recursos a la vez | ¿Cómo evito que todos se queden esperando en círculo? |
| Barbero dormilón | Coordinar un servidor con clientes intermitentes | ¿Cómo duermo cuando no hay trabajo y despierto cuando llega? |
Los cuatro son irreducibles entre sí: saber resolver el productor-consumidor no te dice nada sobre cómo evitar que los filósofos se bloqueen, y saber lo de los filósofos no ayuda con la inanición de escritores. Por eso hay cuatro y no uno.
Y los enunciados con filósofos, barberos y tenedores parecen frívolos, pero la frivolidad es deliberada: un enunciado abstracto ("cinco procesos compiten por cinco recursos compartidos con sus vecinos") obliga a leerlo tres veces, mientras que uno con filósofos que necesitan dos tenedores se entiende a la primera y libera atención para lo que importa, que es el razonamiento sobre la corrección.
Productor-consumidor con búfer limitado
El planteamiento. Un productor genera elementos y los deposita en un búfer de capacidad N. Un consumidor los retira y los procesa. Los dos van a ritmos distintos e impredecibles. Hay que garantizar tres cosas:
- El productor no escribe en un búfer lleno: espera a que haya sitio.
- El consumidor no lee de un búfer vacío: espera a que haya datos.
- Ninguno corrompe el búfer mientras el otro lo toca.
En Meteora es literalmente la relación entre el ingestor y el agregador: el primero recibe lecturas de las 800 estaciones a ráfagas, y el segundo las procesa a su ritmo. Si el búfer fuera infinito, un pico de tráfico agotaría la RAM; si no hubiera búfer, cada lectura tendría que esperar al agregador y se perderían datagramas. El búfer limitado es la solución de ingeniería, y este problema es cómo se implementa correctamente.
Antes de la solución, veamos por qué la versión ingenua no basta. Con un solo mutex:
/* INCORRECTO: protege el búfer pero no coordina los ritmos */
void producir(struct Lectura l) {
pthread_mutex_lock(&m);
if (cuenta == N) { pthread_mutex_unlock(&m); return; } /* ¡pierde la lectura! */
buffer[fin] = l; fin = (fin + 1) % N; cuenta++;
pthread_mutex_unlock(&m);
}El mutex garantiza que el búfer no se corrompe, pero no resuelve la espera. El productor tiene solo dos opciones malas: descartar la lectura (pérdida de datos) o girar en un bucle comprobando cuenta (quemar CPU, y además con el mutex tomado sería un interbloqueo). Lo que falta es una forma de dormir hasta que haya sitio. Ahí entran los semáforos.
La solución con tres semáforos, paso a paso
La solución canónica usa tres primitivas, cada una con un papel bien definido. Esta separación de responsabilidades es lo que hay que entender:
| Primitiva | Valor inicial | Qué representa | Quién hace wait |
Quién hace post |
|---|---|---|---|---|
vacios |
N | Huecos libres en el búfer | Productor | Consumidor |
llenos |
0 | Elementos disponibles | Consumidor | Productor |
mutex |
1 | Acceso exclusivo al búfer | Ambos | Ambos |
La idea clave: vacios y llenos cuentan recursos complementarios. Siempre se cumple vacios + llenos ≤ N, y cuando ambos flujos están fuera de sus secciones críticas, la igualdad es exacta. El productor consume huecos y produce elementos; el consumidor hace lo contrario.
/* prod_cons.c — la cola de Lectura entre el ingestor y el agregador */
#define N 64 /* capacidad del búfer circular */
struct Lectura buffer[N];
int inicio = 0, fin = 0; /* índices del búfer circular */
sem_t vacios; /* huecos libres → inicial N */
sem_t llenos; /* elementos disp. → inicial 0 */
sem_t mutex; /* exclusión mutua → inicial 1 */
void *ingestor(void *arg) { /* PRODUCTOR */
for (int i = 0; ; i++) {
struct Lectura l = { .estacion_id = 41 + (i % 800),
.timestamp = 1756684800 + i,
.temperatura = 21.0f + (i % 50) * 0.1f };
sem_wait(&vacios); /* (1) ¿hay hueco? si no, DUERMO */
sem_wait(&mutex); /* (2) entro en la sección crítica */
buffer[fin] = l; /* (3) deposito */
fin = (fin + 1) % N;
sem_post(&mutex); /* (4) salgo de la sección crítica */
sem_post(&llenos); /* (5) aviso: hay un elemento más */
}
return NULL;
}
void *agregador(void *arg) { /* CONSUMIDOR */
double suma = 0; long n = 0;
while (1) {
sem_wait(&llenos); /* (1) ¿hay elemento? si no, DUERMO */
sem_wait(&mutex); /* (2) entro en la sección crítica */
struct Lectura l = buffer[inicio]; /* (3) retiro */
inicio = (inicio + 1) % N;
sem_post(&mutex); /* (4) salgo de la sección crítica */
sem_post(&vacios); /* (5) aviso: hay un hueco más */
suma += l.temperatura; /* (6) proceso FUERA del mutex */
if (++n % 10000 == 0)
printf("[agregador] %ld lecturas, media %.2f C\n", n, suma / n);
usleep(80); /* simula el coste de agregar */
}
return NULL;
}
/* main(): sem_init(&vacios,0,N), sem_init(&llenos,0,0), sem_init(&mutex,0,1),
crear los dos hilos y hacerles join. */Sigamos el razonamiento paso a paso, porque cada línea está donde está por una razón.
Paso (1) del productor: sem_wait(&vacios). Decrementa el contador de huecos. Si valía 0 —el búfer está lleno—, el hilo se duerme y el núcleo lo saca de la cola de listos. Cuando el consumidor retire un elemento y haga sem_post(&vacios), este hilo despertará. Consumo de CPU mientras espera: cero. Es exactamente la contrapresión que vimos con las tuberías en la lección de IPC, pero implementada por nosotros y con el tamaño que decidamos.
Paso (2): sem_wait(&mutex). Ya sabemos que hay hueco, pero puede que el consumidor esté tocando el búfer ahora mismo; el mutex protege los índices inicio y fin y el contenido del array. Pasos (3) y (4): sección crítica mínima, solo la escritura y el avance del índice: ni la construcción de la Lectura ni el printf están dentro, porque cuanto más corta, menos espera el otro.
Paso (5): sem_post(&llenos). Incrementa el contador de elementos y, si el consumidor estaba dormido esperando datos, lo despierta. Fíjate en que va después del sem_post(&mutex): si fuera antes, el consumidor despertaría e inmediatamente se bloquearía en el mutex que aún tenemos, provocando un despertar inútil y dos cambios de contexto de más.
El consumidor es simétrico, y esa simetría es lo bonito de la solución: espera llenos, toma el mutex, retira, suelta el mutex, y hace post sobre vacios. Cada uno espera lo que el otro produce.
Paso (6) del consumidor: procesar fuera del mutex. El usleep(80) que simula el trabajo de agregación está después de soltar el mutex. Si estuviera dentro, el productor no podría depositar nada mientras el consumidor procesa, y el búfer no serviría absolutamente de nada. Es un error muy común y muy caro.
Al ejecutarlo, el comportamiento observable confirma la teoría: el productor, que podría generar millones de lecturas por segundo, queda limitado a las ~12.500/s que procesa el consumidor, y la memoria usada nunca supera los 64 × 24 = 1.536 bytes del búfer. Sin una sola línea de código dedicada a controlar el ritmo.
La variante con mutex y variable de condición
Los semáforos son elegantes, pero tienen un inconveniente práctico: el estado está repartido entre tres objetos y no puedes inspeccionarlo. Si quieres saber cuántos elementos hay para publicar una métrica, no puedes preguntárselo al semáforo de forma fiable. La variante con mutex y variables de condición mantiene el estado explícito:
# prod_cons.py — la misma cola con un monitor de Python
import threading, time, collections
class ColaLecturas:
def __init__(self, capacidad=64):
self._cap = capacidad
self._buf = collections.deque()
self._lock = threading.Lock()
self._hay_hueco = threading.Condition(self._lock) # ← comparten candado
self._hay_dato = threading.Condition(self._lock)
def depositar(self, lectura):
with self._lock:
while len(self._buf) == self._cap: # ← WHILE, nunca IF
self._hay_hueco.wait()
self._buf.append(lectura)
self._hay_dato.notify() # despierto a UN consumidor
def retirar(self):
with self._lock:
while not self._buf: # ← WHILE, nunca IF
self._hay_dato.wait()
l = self._buf.popleft()
self._hay_hueco.notify() # despierto a UN productor
return l
def ocupacion(self): # ← esto NO se puede con semáforos
with self._lock:
return len(self._buf), self._cap
# El ingestor llama a cola.depositar(lectura) en bucle; el agregador,
# a cola.retirar() seguido del trabajo de agregación. Y desde fuera:
# n, cap = cola.ocupacion(); print(f"ocupacion: {n}/{cap}")Tres detalles de diseño que merecen atención:
Dos variables de condición, un solo candado. threading.Condition(self._lock) hace que ambas compartan el mismo Lock, y es imprescindible porque el estado que vigilan es el mismo búfer: usar dos candados distintos rompería la exclusión mutua.
Dos condiciones en lugar de una. Podría usarse una sola con notify_all(), pero entonces cada notificación despertaría también a hilos que esperan la condición contraria, que comprobarían su while, verían que no y volverían a dormir. Con dos, cada notify() despierta exactamente al tipo de hilo correcto; con 8 productores y 8 consumidores, la diferencia de rendimiento es de un factor 3 o 4.
ocupacion() es la ventaja decisiva de esta variante. Poder responder "el búfer está al 62/64" permite alertar de que el consumidor no da abasto antes de que empiece a haber pérdidas. Es la métrica de causa de la que hablamos al cerrar el módulo 2, y con semáforos no la tienes.
Comparadas:
| Tres semáforos | Mutex + variables de condición | |
|---|---|---|
| Estado inspeccionable | No | Sí |
| Condiciones de espera complejas | Difícil | Natural (cualquier predicado) |
Riesgo de invertir los wait |
Alto (interbloqueo) | Bajo |
| Rendimiento | Ligeramente mejor | Muy similar |
| Disponible en lenguajes de alto nivel | A veces | Siempre |
La recomendación práctica: usa mutex y variables de condición salvo que el problema encaje exactamente en el molde de contar recursos. Es más verboso, pero expresa la condición de espera de forma explícita, permite condiciones arbitrarias y no tiene la trampa del apartado siguiente.
La pérdida de señal y el orden de los wait
Dos trampas de este problema merecen su propio apartado porque son las que de verdad cuelgan sistemas.
El orden de los wait en la solución con semáforos
Mira otra vez el productor y prueba a intercambiar las dos primeras líneas:
/* ⚠ INTERBLOQUEO GARANTIZADO */
sem_wait(&mutex); /* (1) primero tomo el mutex */
sem_wait(&vacios); /* (2) y LUEGO espero a que haya hueco */Traza del desastre, con el búfer lleno:
| Tiempo | Productor | Consumidor | mutex |
vacios |
|---|---|---|---|---|
| t1 | sem_wait(&mutex) → entra |
0 | 0 | |
| t2 | sem_wait(&vacios) → 0: duerme |
0 | 0 | |
| t3 | sem_wait(&llenos) → pasa |
0 | 0 | |
| t4 | sem_wait(&mutex) → 0: duerme |
0 | 0 | |
| t5 | dormido con el mutex tomado | dormido esperando el mutex | 0 | 0 |
Interbloqueo total. El productor duerme esperando un hueco sin haber soltado el mutex; el consumidor, que es el único que puede crear ese hueco, no puede entrar porque el mutex está tomado. Ninguno despertará jamás. De aquí sale una regla que vale para toda la programación concurrente:
Nunca te bloquees esperando una condición mientras tienes un candado que otro necesita para hacerla cierta.
En la solución correcta, el sem_wait(&vacios) ocurre antes de tomar el mutex, así que si el productor duerme, duerme sin bloquear a nadie; y en el consumidor, sem_wait(&llenos) antes que sem_wait(&mutex). La regla mnemotécnica es contar antes de entrar. Fíjate en el contraste: la variante con variables de condición no tiene esta trampa, porque pthread_cond_wait suelta el mutex automáticamente al dormirse. Ese es exactamente el problema para el que se inventaron.
La pérdida de señal
La segunda trampa afecta a las variables de condición. Consideremos esta versión incorrecta del consumidor:
/* ⚠ SEÑAL PERDIDA */
if (cuenta == 0) /* (1) compruebo: está vacío */
pthread_mutex_unlock(&m); /* (2) suelto el mutex */
pthread_cond_wait(&hay_dato, &m); /* (3) y me duermo */Entre (2) y (3) hay una ventana. Si el productor deposita un elemento justo ahí y hace signal, no hay nadie durmiendo todavía: la señal se pierde en el vacío. Cuando el consumidor llegue a (3), se dormirá esperando un aviso que ya se emitió, y si el productor no vuelve a producir, dormirá para siempre con un elemento disponible en el búfer.
La solución está integrada en la primitiva: pthread_cond_wait(&cond, &mutex) suelta el mutex y encola al hilo de forma atómica, sin ninguna ventana entre ambas cosas. Por eso hay que pasarle el mutex; por eso el mutex debe estar tomado al llamarla; y por eso nunca se suelta a mano antes.
Existe además la variante de pérdida de señal que resuelve el while: si el consumidor despierta pero otro consumidor le ganó el elemento, la condición vuelve a ser falsa. Con if seguiría adelante sobre un búfer vacío; con while vuelve a dormir. Ambos mecanismos —la atomicidad de wait y el bucle while— son necesarios, y protegen contra cosas distintas.
Lectores-escritores: el planteamiento
El planteamiento. Un recurso compartido es accedido por dos tipos de flujo: lectores, que solo consultan y pueden hacerlo varios a la vez sin problema, y escritores, que modifican y necesitan acceso exclusivo sin otros escritores ni lectores. La asimetría es todo el problema: con un mutex simple sería trivial (uno cada vez), pero desperdiciaría el paralelismo entre lectores. En Meteora es la relación entre meteo-api y el agregador sobre /dev/shm/meteora-cache:
| Flujo | Papel | Frecuencia | Duración |
|---|---|---|---|
4 trabajadores de meteo-api |
Lectores | 1.200/s | ~40 µs (recorren ultimas[]) |
agregador |
Escritor | 1/hora | ~15 ms (recalcula todas las medias) |
Con un mutex simple, los 1.200 accesos por segundo se serializarían: 1.200 × 40 µs = 48 ms de CPU por segundo en un solo núcleo, con los otros tres trabajadores esperando; con acceso compartido, los cuatro leen a la vez y el coste real es 12 ms por núcleo.
Formulado con precisión, el problema exige varios lectores simultáneos si no hay escritor, y un escritor en exclusiva sin otros escritores ni lectores. Y aquí está el conflicto que no tiene respuesta única: ¿quién tiene preferencia cuando ambos esperan? De ahí salen las dos variantes clásicas.
Prioridad a lectores y la inanición de escritores
La primera solución de Courtois (1971) da preferencia a los lectores: si hay lectores dentro, un lector nuevo entra sin esperar, aunque haya un escritor en la cola.
/* lectores_escritores_v1.c — prioridad a LECTORES */
sem_t recurso; /* acceso exclusivo al recurso; inicial 1 */
sem_t mutex_cuenta; /* protege n_lectores; inicial 1 */
int n_lectores = 0;
void *lector(void *arg) {
sem_wait(&mutex_cuenta);
n_lectores++;
if (n_lectores == 1) sem_wait(&recurso); /* PRIMER lector: cierro a escritores */
sem_post(&mutex_cuenta);
leer_cache(); /* ---- LECTURA: varios a la vez aquí ---- */
sem_wait(&mutex_cuenta);
n_lectores--;
if (n_lectores == 0) sem_post(&recurso); /* ÚLTIMO lector: abro a escritores */
sem_post(&mutex_cuenta);
return NULL;
}
void *escritor(void *arg) {
sem_wait(&recurso); /* espero a que NO haya lectores ni escritores */
recalcular_medias(&cache); /* ---- ESCRITURA: yo solo ---- */
sem_post(&recurso);
return NULL;
}El mecanismo, llamado candado del portero, es ingenioso: solo el primer lector adquiere el semáforo recurso y solo el último en salir lo libera, así que los intermedios entran y salen libremente mientras haya al menos uno dentro. El escritor, mientras tanto, ve el recurso ocupado durante todo ese tiempo.
Aquí está el problema, y es grave. Traza con lectores que llegan continuamente:
| Tiempo | Evento | n_lectores |
Estado del escritor |
|---|---|---|---|
| t1 | Llega el lector A | 1 | – |
| t2 | Llega el escritor | 1 | espera en sem_wait(&recurso) |
| t3 | Llega el lector B (¡pasa por delante!) | 2 | espera |
| t4 | Sale A | 1 | espera |
| t5 | Llega el lector C | 2 | espera |
| t6 | Sale B | 1 | espera |
| t7 | Llega el lector D | 2 | espera |
| ... | nunca hay un instante con n_lectores == 0 |
≥1 | espera para siempre |
Esto es inanición de escritores, y basta con que los lectores lleguen con más frecuencia que su duración para que nunca haya un hueco. No es un caso raro de laboratorio: midiendo con 8 lectores continuos y 1 escritor durante 30 segundos, la v1 completa 3 escrituras, con una espera máxima de 11,4 segundos. Para Meteora eso significaría servir datos de hace once segundos con toda normalidad. Inaceptable.
El problema conceptual es que esta solución cumple exclusión mutua y progreso pero viola la espera limitada —el tercer requisito de Dijkstra que enunciamos en 03-01—: no hay ningún límite al número de lectores que pueden adelantar al escritor.
Prioridad a escritores y qué hace rwlock de verdad
La segunda solución de Courtois invierte la preferencia: en cuanto un escritor anuncia que quiere entrar, ningún lector nuevo pasa. Los que ya están dentro terminan, y el escritor entra a continuación.
/* lectores_escritores_v2.c — prioridad a ESCRITORES */
sem_t recurso; /* inicial 1 */
sem_t mutex_lect; /* protege n_lectores; inicial 1 */
sem_t mutex_escr; /* protege n_escritores; inicial 1 */
sem_t cola; /* PUERTA: bloquea a los lectores nuevos; inicial 1 */
int n_lectores = 0, n_escritores = 0;
void *lector(void *arg) {
sem_wait(&cola); /* (1) ¿hay escritor esperando? entonces paro aquí */
sem_wait(&mutex_lect);
n_lectores++;
if (n_lectores == 1) sem_wait(&recurso);
sem_post(&mutex_lect);
sem_post(&cola); /* (2) libero la puerta enseguida */
leer_cache(); /* ---- LECTURA compartida ---- */
sem_wait(&mutex_lect);
n_lectores--;
if (n_lectores == 0) sem_post(&recurso);
sem_post(&mutex_lect);
return NULL;
}
void *escritor(void *arg) {
sem_wait(&mutex_escr);
n_escritores++;
if (n_escritores == 1) sem_wait(&cola); /* (3) CIERRO la puerta a lectores nuevos */
sem_post(&mutex_escr);
sem_wait(&recurso); /* (4) espero a que salgan los lectores actuales */
recalcular_medias(&cache); /* ---- ESCRITURA exclusiva ---- */
sem_post(&recurso);
sem_wait(&mutex_escr);
n_escritores--;
if (n_escritores == 0) sem_post(&cola); /* (5) reabro la puerta */
sem_post(&mutex_escr);
return NULL;
}La pieza nueva es el semáforo cola, que actúa de puerta de entrada: cuando el primer escritor llega (línea 3) la cierra, los lectores que lleguen después se quedan bloqueados en la línea (1) sin haber tocado n_lectores, los que ya estaban dentro terminan, n_lectores llega a 0, se libera recurso y el escritor entra en la línea (4). Los números cambian radicalmente:
Con el mismo experimento de antes, la v2 completa 29.847 escrituras con una espera máxima de 1,2 ms, frente a las 3 escrituras y los 11,4 segundos de la v1, a cambio de un 2,3 % menos de lecturas. Un intercambio evidentemente bueno. Pero ahora el riesgo se invierte: si los escritores llegan continuamente, los lectores pasan hambre, porque la puerta nunca se reabre. La tercera solución (Hoare, 1974) alterna estrictamente los turnos y no produce inanición de ninguno de los dos tipos, a costa de más complejidad.
Qué hace pthread_rwlock_t de verdad
Ahora que has visto las dos soluciones, la pregunta natural es qué implementa realmente la primitiva que usaste en la lección anterior. La respuesta importa porque el comportamiento por defecto no es el que la gente supone:
| Implementación | Comportamiento por defecto | Cómo cambiarlo |
|---|---|---|
| glibc / Linux | Prioridad a lectores (v1: escritores pueden pasar hambre) | pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP) |
| macOS | Prioridad a escritores | No configurable |
| Windows (SRW) | Sin garantía de orden | No configurable |
std::shared_mutex (C++17) |
No especificado por el estándar | Depende de la implementación |
Es decir: un pthread_rwlock_t en Linux, tal cual, tiene exactamente el problema de inanición que acabamos de medir. Si tu escritor es poco frecuente pero necesita ejecutarse a tiempo —como el agregador de Meteora, que debe publicar las medias en cuanto las calcula—, tienes que pedir explícitamente la prioridad de escritores:
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
pthread_rwlock_init(&cache.cerrojo, &attr);El sufijo NONRECURSIVE avisa de la contrapartida: con esta configuración, un hilo que ya tiene el bloqueo de lectura y pide otro puede quedarse bloqueado si hay un escritor esperando. Ese autobloqueo es la razón de que no sea el valor por defecto; si tu código nunca anida bloqueos de lectura —y no debería—, es seguro.
Y para el caso extremo de Meteora, con 4.320.000 lecturas por escritura, la mejor solución no es ningún rwlock sino el doble búfer: el agregador construye una caché nueva completa y, al terminar, publica su puntero con una escritura atómica en modo release; los lectores hacen una lectura acquire del puntero y trabajan sobre la versión que les tocó, sin tomar ningún candado. Cero contención para los lectores, y el escritor nunca espera. El coste es la memoria de dos copias y decidir cuándo liberar la vieja, que es el problema que resuelve RCU dentro del núcleo de Linux.
Filósofos comensales
El planteamiento. Cinco filósofos se sientan alrededor de una mesa redonda. Entre cada par hay un tenedor: cinco tenedores en total. Cada filósofo alterna entre pensar y comer, y para comer necesita los dos tenedores adyacentes, el de su izquierda y el de su derecha.
graph TD
F0((Filósofo 0)) --- T0[Tenedor 0] --- F1((Filósofo 1))
F1 --- T1[Tenedor 1] --- F2((Filósofo 2))
F2 --- T2[Tenedor 2] --- F3((Filósofo 3))
F3 --- T3[Tenedor 3] --- F4((Filósofo 4))
F4 --- T4[Tenedor 4] --- F0
Lo que este problema aísla, y ninguno de los anteriores contiene, es la adquisición de varios recursos a la vez: con un solo recurso no hay dificultad; con dos, aparece el interbloqueo. La solución ingenua es la que escribiría cualquiera:
/* ⚠ SE BLOQUEA. Con paciencia, siempre. */
sem_t tenedor[5]; /* cada uno inicializado a 1 */
void *filosofo(void *arg) {
int i = (int)(long)arg;
while (1) {
pensar();
sem_wait(&tenedor[i]); /* (1) tomo el tenedor de mi izquierda */
sem_wait(&tenedor[(i + 1) % 5]); /* (2) tomo el de mi derecha */
comer();
sem_post(&tenedor[i]);
sem_post(&tenedor[(i + 1) % 5]);
}
}Por qué se bloquea. Si los cinco filósofos ejecutan la línea (1) antes de que ninguno llegue a la (2) —lo que ocurre en cuanto el planificador los alterna en ese punto—, cada uno tiene un tenedor y espera el de su vecino: el 0 tiene el tenedor 0 y espera el 1, que tiene el filósofo 1; el 1 espera el 2; el 2 el 3; el 3 el 4; y el filósofo 4 espera el tenedor 0, que tiene el filósofo 0.
La cadena de esperas se cierra en un ciclo, y ninguno soltará su tenedor porque todos están bloqueados esperando el segundo. Es un interbloqueo de manual. Por qué exactamente se produce —qué cuatro condiciones deben cumplirse simultáneamente para que un ciclo así sea posible, y cómo romper cada una— es el contenido de Interbloqueos, que también resolverá este caso con prevención sistemática. Aquí nos quedamos con las tres soluciones prácticas que se usan de verdad.
Solución 1: asimetría (la más usada). Que los filósofos pares tomen primero el izquierdo y los impares primero el derecho:
void *filosofo(void *arg) {
int i = (int)(long)arg;
int izq = i, der = (i + 1) % 5;
while (1) {
pensar();
if (i % 2 == 0) { sem_wait(&tenedor[izq]); sem_wait(&tenedor[der]); }
else { sem_wait(&tenedor[der]); sem_wait(&tenedor[izq]); }
comer();
sem_post(&tenedor[izq]); sem_post(&tenedor[der]);
}
}Con esta modificación, el ciclo de espera es imposible: al menos dos filósofos adyacentes compiten por el mismo tenedor como primera petición, y uno de los dos lo consigue y avanza. Es la manifestación concreta de la regla de ingeniería más importante contra los interbloqueos: tomar siempre los recursos en un orden global consistente. Aquí se logra numerando los tenedores y haciendo que todos pidan primero el de número menor —que es exactamente lo que produce el reparto par/impar—.
Solución 2: un filósofo menos. Permitir que como mucho cuatro se sienten a la vez, con un semáforo contador:
sem_t sitios; /* sem_init(&sitios, 0, 4) — ¡4, no 5! */
sem_wait(&sitios); /* como mucho 4 intentan comer a la vez */
sem_wait(&tenedor[izq]);
sem_wait(&tenedor[der]);
comer();
sem_post(&tenedor[der]); sem_post(&tenedor[izq]);
sem_post(&sitios);El razonamiento es de conteo puro: con 4 filósofos compitiendo por 5 tenedores, por el principio del palomar al menos uno consigue los dos, podrá comer, soltarlos y desatascar la cadena. Un solo cambio de 5 a 4 elimina el interbloqueo por completo, y es la solución más fácil de verificar y la que menos código toca.
Solución 3: tomar ambos tenedores atómicamente. Un mutex global que protege la operación de coger los dos:
pthread_mutex_t mesa = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t puedo[5];
int libre[5] = {1,1,1,1,1};
void tomar_tenedores(int i) {
int izq = i, der = (i + 1) % 5;
pthread_mutex_lock(&mesa);
while (!libre[izq] || !libre[der]) /* ← o los DOS, o ninguno */
pthread_cond_wait(&puedo[i], &mesa);
libre[izq] = libre[der] = 0;
pthread_mutex_unlock(&mesa);
}
void soltar_tenedores(int i) {
int izq = i, der = (i + 1) % 5;
pthread_mutex_lock(&mesa);
libre[izq] = libre[der] = 1;
pthread_cond_signal(&puedo[(i + 4) % 5]); /* aviso a mis dos vecinos */
pthread_cond_signal(&puedo[(i + 1) % 5]);
pthread_mutex_unlock(&mesa);
}Aquí se elimina la raíz del problema: un filósofo nunca llega a tener un solo tenedor, porque o consigue los dos en una operación atómica o no consigue ninguno y duerme. Sin adquisición parcial no hay retención de recursos, y sin retención no hay ciclo posible. Comparadas:
| Solución | Elimina el interbloqueo | Concurrencia | Complejidad | ¿Inanición? |
|---|---|---|---|---|
| Ingenua | No | – | Mínima | – |
| Asimetría par/impar | Sí | Alta | Muy baja | Posible en teoría |
| Un filósofo menos | Sí | Media (4 de 5) | Mínima | No |
| Ambos a la vez | Sí | Alta | Media | Posible sin turnos |
La traducción a Meteora es directa y nada teórica. Si el agregador toma primero el candado de /dev/shm/meteora-cache y luego el del fichero del día, mientras un trabajador de meteo-api los toma en el orden inverso, tienes exactamente cinco filósofos con dos tenedores. Y ese caso concreto, con su diagnóstico y su solución, es el que resolveremos en la lección siguiente.
El barbero dormilón
El planteamiento. Una barbería tiene un barbero, un sillón y N sillas de espera. Si no hay clientes, el barbero se duerme; si llega uno y el barbero duerme, lo despierta; si el barbero está ocupado, el cliente se sienta a esperar si hay silla libre, y si no la hay, se marcha. Este problema aísla algo que los otros no tocan: la coordinación entre un servidor y clientes que llegan de forma intermitente, incluyendo dormir cuando no hay trabajo sin quemar CPU, despertar cuando llega, y rechazar carga cuando se supera la capacidad.
/* barbero.c — un grupo de trabajadores esperando peticiones */
#define N_SILLAS 20 /* cola de peticiones pendientes */
sem_t clientes; /* peticiones esperando; inicial 0 */
sem_t barberos; /* trabajadores libres; inicial 0 */
sem_t mutex; /* protege el contador; inicial 1 */
int esperando = 0;
void *trabajador(void *arg) { /* el BARBERO */
while (1) {
sem_wait(&clientes); /* (1) si no hay peticiones, DUERMO */
sem_wait(&mutex);
esperando--; /* (2) tomo una de la cola */
sem_post(&mutex);
sem_post(&barberos); /* (3) aviso: estoy listo para atenderte */
atender_peticion(); /* (4) trabajo de verdad */
}
return NULL;
}
void *peticion_http(void *arg) { /* el CLIENTE */
sem_wait(&mutex);
if (esperando < N_SILLAS) { /* (5) ¿hay hueco en la cola? */
esperando++;
sem_post(&clientes); /* (6) despierto a un trabajador */
sem_post(&mutex);
sem_wait(&barberos); /* (7) espero a que uno me atienda */
} else {
sem_post(&mutex);
responder_503(); /* (8) cola llena: rechazo la petición */
}
return NULL;
}Los puntos que importan:
El barbero duerme sin consumir CPU (1). sem_wait(&clientes) con el contador a 0 bloquea al hilo, que sale de la cola de listos. Con 4 trabajadores y ninguna petición, meteo-api consume 0 % de CPU. Es la diferencia entre un servicio que se puede desplegar y uno que quema cuatro núcleos sin hacer nada.
El cliente comprueba el aforo antes de encolarse (5). Este detalle es lo que convierte el problema clásico en un patrón de ingeniería serio: rechazar carga es una decisión de diseño, no un fallo. Si la cola está llena, responder un 503 inmediato es mucho mejor que aceptar la petición y hacer esperar 30 segundos a un cliente que ya habrá abandonado. Es el load shedding que sostiene los servicios bajo picos.
El doble semáforo clientes/barberos es un encuentro (6, 7, 3): el cliente avisa de que ha llegado y espera confirmación, y el trabajador toma la petición y confirma que la atiende; sin ese doble sentido, un cliente podría no saber nunca si alguien lo ha cogido. Y el contador esperando va protegido por mutex (2, 5) porque es un check-then-act de libro: comprobar el aforo y encolarse deben ser indivisibles, o dos clientes pasarían la comprobación con una sola silla libre.
La correspondencia con Meteora es exacta, y explica el grupo de hilos de la lección 03-02:
| Barbería | meteo-api |
|---|---|
| Barbero / sillón | Hilo trabajador del grupo, atendiendo |
| Sillas de espera (N) | Cola de peticiones pendientes |
| Cliente que llega | Petición HTTP entrante |
| Barbero dormido | Hilo bloqueado en la cola: 0 % CPU |
| Cliente que se marcha | Respuesta 503 Service Unavailable |
El dimensionado de N es la decisión de ingeniería. Con 4 trabajadores a 40 µs por petición, la capacidad es de 100.000 peticiones/s. Si N = 20 y llegan a 1.200/s, la cola nunca se llena en operación normal, pero absorbe ráfagas de hasta 20 peticiones simultáneas. Un N demasiado grande —10.000, por ejemplo— es peor que uno pequeño: acepta peticiones que tardarán segundos en atenderse, cuando el cliente ya habrá agotado su tiempo límite. Una cola grande no aumenta la capacidad, solo aumenta la latencia y esconde el problema.
Qué patrón real corresponde a cada problema
Esta tabla convierte la teoría en herramienta de trabajo: cuando te encuentres con uno de estos escenarios, ya sabes qué problema clásico estás resolviendo y qué solución probar.
| Problema clásico | Patrón real | Ejemplos concretos |
|---|---|---|
| Productor-consumidor | Cola de trabajos entre etapas | ingestor→agregador; colas de mensajes (RabbitMQ, Kafka); tuberías del shell; BlockingQueue; canales de Go; búfer de sockets del núcleo |
| Lectores-escritores | Datos leídos mucho, escritos poco | meteo-api sobre la caché; caché de configuración; tabla de rutas del núcleo; índices de base de datos; DNS local |
| Filósofos comensales | Adquirir varios recursos | Transferencias entre dos cuentas bancarias; agregador + meteo-api con dos candados; transacciones que bloquean varias filas; asignación de dispositivos |
| Barbero dormilón | Grupo de trabajadores con cola acotada | ThreadPoolExecutor; trabajadores de nginx; pool de conexiones a base de datos; accept() sobre un socket con backlog |
Y las señales de alarma que deben hacerte pensar en cada uno:
- "Se nos llena la memoria cuando hay un pico de tráfico" → productor-consumidor sin búfer limitado. Falta contrapresión.
- "La actualización tarda muchísimo en aplicarse, pero las consultas van rápido" → lectores-escritores con inanición del escritor. Revisa la política del
rwlock. - "El servicio se cuelga aleatoriamente, y solo bajo carga" → filósofos: dos candados tomados en orden inverso.
- "Los trabajadores consumen CPU aunque no haya peticiones" → barbero mal implementado con espera activa en lugar de bloqueo.
Errores Comunes y Consejos
Invertir el orden de los wait en el productor-consumidor. Tomar el mutex antes que el semáforo de conteo produce un interbloqueo garantizado, como vimos en la traza. La regla: contar antes de entrar, y nunca bloquearse esperando una condición mientras tienes un candado que otro necesita para hacerla cierta.
Procesar el elemento dentro de la sección crítica. Si el consumidor procesa con el mutex tomado, el productor no puede depositar y el búfer no sirve de nada: has convertido un sistema desacoplado en uno estrictamente alterno. Retira el elemento, suelta el candado, y procesa fuera.
Suponer que pthread_rwlock_t protege al escritor de la inanición. En glibc, el comportamiento por defecto es prioridad a lectores, y con lecturas continuas el escritor puede esperar segundos, como medimos. Si tu escritor tiene requisitos de latencia, pide PREFER_WRITER_NONRECURSIVE_NP explícitamente.
Tomar dos candados en órdenes distintos en sitios distintos. Es el problema de los filósofos disfrazado, y es la causa número uno de cuelgues en producción. Define un orden global —por dirección de memoria, por identificador, por nivel— y respétalo en todo el código sin excepciones.
Usar una cola sin límite "para no perder nada". Una cola ilimitada convierte un problema de rendimiento en uno de memoria: el proceso crece hasta que el OOM killer lo mata (módulo 2) y entonces se pierde todo, no solo el excedente. Un límite explícito con rechazo o bloqueo es siempre mejor.
Consejo: nombra el patrón en el código. Un comentario /* productor-consumidor con búfer limitado; ver 03-05 */ sobre la estructura ahorra media hora a quien lo lea después. El vocabulario común solo sirve si se usa.
Consejo: prefiere las primitivas de tu biblioteca a reimplementarlas. queue.Queue de Python, BlockingQueue de Java, los canales de Go y pthread_rwlock_t ya resuelven estos problemas, están probados por millones de ejecuciones y a menudo tienen optimizaciones que tú no harías. Estudia los problemas clásicos para entender qué hace tu biblioteca y elegir bien, no para reescribirla.
Ejercicios
Ejercicio 1: medir la contrapresión
Implementa el productor-consumidor con tres semáforos y búfer de 64 posiciones, con un productor rápido (sin retardo) y un consumidor lento (100 µs por elemento). Instrumenta el código para medir cuántos elementos hay en el búfer cada 100 ms durante 5 segundos y cuál es el ritmo real del productor. Después repite con un búfer de 4 y otro de 4096 posiciones, y explica qué cambia y qué no.
Ejercicio 2: provocar y medir la inanición
Implementa lectores-escritores con las dos soluciones de Courtois. Lanza 8 lectores que leen continuamente (200 µs por lectura) y 1 escritor que intenta escribir cada 100 ms. Mide durante 30 segundos: escrituras completadas, espera máxima del escritor y lecturas completadas. Compara ambas soluciones y calcula el precio en lecturas que se paga por evitar la inanición.
Ejercicio 3: filósofos que se bloquean
Implementa la solución ingenua de los filósofos y añade un mecanismo que detecte el interbloqueo: un hilo vigilante que compruebe cada segundo si ningún filósofo ha comido en los últimos 3 segundos y lo notifique. Ejecuta el programa hasta que se bloquee y anota cuánto tarda. Después implementa las tres soluciones (asimetría, un filósofo menos, ambos tenedores a la vez), mide cuántas comidas por segundo consigue cada una y explica las diferencias.
Soluciones
Solución 1
/* contrapresion.c — el hilo que instrumenta el búfer. El productor incrementa
'producidos' tras su sem_post(&llenos); el consumidor, 'consumidos' tras
su sem_post(&vacios). Ambos son _Atomic long. */
void *vigilante(void *arg) {
long ant_p = 0;
for (int t = 0; t < 50; t++) {
usleep(100000);
long p = atomic_load(&producidos), c = atomic_load(&consumidos);
printf("t=%.1fs ocupacion=%ld/%d ritmo_prod=%ld/s\n",
t * 0.1, p - c, CAP, (p - ant_p) * 10);
ant_p = p;
}
return NULL;
}Resultados con consumidor a 100 µs por elemento (máximo teórico: 10.000/s):
| Capacidad | Ocupación en régimen | Ritmo del productor | Memoria del búfer | Latencia de un elemento |
|---|---|---|---|---|
| 4 | 4/4 (siempre llena) | 9.998/s | 96 B | 0,4 ms |
| 64 | 64/64 (siempre llena) | 9.998/s | 1,5 KB | 6,4 ms |
| 4096 | 4096/4096 | 9.998/s | 98 KB | 409 ms |
Qué cambia y qué no. Lo que no cambia es lo importante: el ritmo del productor es el mismo en los tres casos, 9.998 elementos por segundo, es decir, exactamente el ritmo del consumidor. El búfer no aumenta la capacidad del sistema ni un elemento, porque el cuello de botella es el consumidor y ningún tamaño de búfer lo acelera. Lo que sí cambia es la memoria y, sobre todo, la latencia: con 4096 posiciones siempre llenas, un elemento tarda 4096 × 100 µs = 409 milisegundos en salir, frente a 0,4 ms con 4 posiciones. Mil veces peor por usar un búfer mil veces más grande.
La conclusión es contundente y contraria a la intuición: un búfer más grande no hace el sistema más rápido, lo hace más lento en latencia y esconde el problema real. El búfer solo sirve para absorber ráfagas —picos temporales por encima de la media— y su tamaño debe dimensionarse por la duración esperada de la ráfaga, no "por si acaso". Si en Meteora las 800 estaciones envían en el mismo segundo, un búfer de 800 está justificado; uno de 100.000 solo garantiza que los datos lleguen tarde.
Solución 2
Mediciones sobre meteo-01, 8 lectores de 200 µs, 1 escritor cada 100 ms, 30 segundos:
| v1 (prioridad lectores) | v2 (prioridad escritores) | |
|---|---|---|
| Lecturas completadas | 1.198.412 | 1.161.238 |
| Escrituras completadas | 3 | 298 |
| Espera media del escritor | 7,8 s | 0,4 ms |
| Espera máxima del escritor | 11,4 s | 1,2 ms |
| Escrituras esperadas (30 s / 100 ms) | 300 | 300 |
Análisis. La v1 completa 3 escrituras de las 300 intentadas: un 1 %. El escritor pide el recurso y, mientras espera, llegan lectores nuevos que pasan por delante gracias al candado del portero; con 8 lectores de 200 µs, la probabilidad de que haya un instante con cero lectores es tan baja que el escritor espera segundos enteros. La v2 completa 298 de 300, un 99,3 %, con una espera máxima de 1,2 ms —lo que tardan en terminar los lectores que ya estaban dentro cuando cerró la puerta—.
El precio de evitar la inanición es de 37.174 lecturas, un 3,1 %: lo que cuesta detener a los lectores nuevos mientras el escritor espera y trabaja. El intercambio es obviamente bueno —multiplicar por 99 las escrituras y bajar la latencia del escritor por un factor de 9.500— y por eso la recomendación de la lección anterior era configurar PREFER_WRITER_NONRECURSIVE_NP explícitamente en Linux. Un apunte útil: si en tu caso los escritores también fueran frecuentes, la v2 produciría inanición de lectores y necesitarías la solución de Hoare con turnos alternos, o simplemente un mutex normal, porque con lecturas y escrituras equilibradas el rwlock ya no compensa (03-04).
Solución 3
/* filosofos.c — el hilo vigilante */
_Atomic long ultima_comida[5], comidas_totales = 0;
void *vigilante(void *arg) {
while (1) {
sleep(1);
long ahora = time(NULL), max_inactivo = 0;
for (int i = 0; i < 5; i++) {
long inact = ahora - atomic_load(&ultima_comida[i]);
if (inact > max_inactivo) max_inactivo = inact;
}
if (max_inactivo >= 3) {
printf("⚠ POSIBLE INTERBLOQUEO: nadie come desde hace %ld s "
"(%ld comidas)\n", max_inactivo, atomic_load(&comidas_totales));
return NULL;
}
}
}Ejecutando tres veces la solución ingenua:
$ ./filosofos --ingenua → ⚠ POSIBLE INTERBLOQUEO ... (1.841 comidas) $ ./filosofos --ingenua → ⚠ POSIBLE INTERBLOQUEO ... (12 comidas) $ ./filosofos --ingenua → ⚠ POSIBLE INTERBLOQUEO ... (94.203 comidas)
El tiempo hasta el bloqueo es completamente impredecible: 12 comidas en una ejecución, 94.203 en otra. Es el no determinismo de la lección 03-01 en estado puro, y explica por qué estos fallos pasan las pruebas y aparecen en producción una madrugada. Con pensar() y comer() más largos, el bloqueo tarda más en llegar; si un desarrollador prueba con retardos generosos y producción los tiene cortos, la diferencia puede ser de días a segundos.
Rendimiento de las tres soluciones (comidas por segundo, 10 segundos, comer() de 1 ms):
| Solución | Comidas/s | Relación | Comentario |
|---|---|---|---|
| Ingenua | – | – | Se bloquea |
| Asimetría par/impar | 1.987 | 1,00× | Referencia |
| Un filósofo menos | 1.962 | 0,99× | Prácticamente igual |
| Ambos tenedores a la vez | 1.943 | 0,98× | El mutex global cuesta un poco |
Interpretación. Las tres funcionan y rinden casi igual, en torno a 1.950-1.990 comidas por segundo, sobre un máximo teórico de 2.000/s: con cinco filósofos y cinco tenedores, como mucho dos pueden comer simultáneamente (dos no adyacentes usan 4 tenedores y el quinto se queda sin par), así que 2 × 1.000 = 2.000/s. Están al 97-99 % del óptimo. Las diferencias son pequeñas pero explicables: la asimetría no añade ninguna primitiva, solo cambia el orden, así que es la más rápida; un filósofo menos añade un sem_wait por comida, que aquí apenas se nota porque el límite real ya era 2; y ambos a la vez serializa la toma de tenedores en un mutex global, lo que sí introduce contención medible.
Criterio de elección: la asimetría es la mejor opción general, porque no cuesta nada y es la aplicación directa del orden global de bloqueos, la técnica que usarás en código real. La de "un filósofo menos" es la más fácil de verificar formalmente. Y la de "ambos a la vez" es la adecuada cuando el número de recursos es variable o no se pueden ordenar de forma natural.
Conclusión
Los cuatro problemas clásicos son el vocabulario común de la concurrencia porque cada uno aísla una dificultad irreducible: coordinar ritmos con recursos finitos, repartir un recurso entre accesos asimétricos, adquirir varios recursos a la vez, y coordinar un servidor con clientes intermitentes.
El productor-consumidor con búfer limitado se resuelve con tres semáforos —vacios a N, llenos a 0 y mutex a 1— donde los dos primeros cuentan recursos complementarios y el productor consume huecos mientras el consumidor consume elementos. Su trampa mortal es el orden de los wait: contar antes de entrar, porque tomar el mutex antes de esperar el hueco produce un interbloqueo inmediato en cuanto el búfer se llena. La variante con mutex y variables de condición evita esa trampa por construcción, permite condiciones de espera arbitrarias y —decisivo en producción— deja inspeccionar la ocupación, que es la métrica que avisa de un consumidor saturado antes de que haya pérdidas. Y hemos medido lo que casi nadie espera: un búfer más grande no acelera nada, solo multiplica la latencia (409 ms con 4096 posiciones frente a 0,4 ms con 4) y esconde el problema.
Los lectores-escritores exponen un conflicto sin respuesta única. Con prioridad a lectores, el candado del portero deja que solo el primero cierre la puerta y el último la abra, pero produce inanición de escritores: 3 escrituras en 30 segundos y esperas de 11,4 segundos, medidas. Con prioridad a escritores, un semáforo de puerta detiene a los lectores nuevos en cuanto un escritor se anuncia: 298 escrituras y 1,2 ms de espera máxima, a cambio de un 3,1 % menos de lecturas. Y el dato que hay que llevarse: pthread_rwlock_t en glibc implementa por defecto la versión con inanición, así que hay que pedir PREFER_WRITER_NONRECURSIVE_NP a mano. Para la proporción extrema de Meteora, la mejor solución no es ningún rwlock sino el doble búfer con publicación atómica del puntero, donde los lectores no toman ningún candado.
Los filósofos comensales aíslan la adquisición de varios recursos, y su solución ingenua se bloquea de forma impredecible —12 comidas en una ejecución, 94.203 en otra—, que es la razón de que estos fallos superen las pruebas. Las tres soluciones prácticas rinden casi idéntico (97-99 % del óptimo teórico de 2.000 comidas/s): la asimetría par/impar, que es la aplicación directa del orden global de bloqueos; un filósofo menos, que por el principio del palomar garantiza que alguien avance; y tomar ambos tenedores atómicamente, que impide la adquisición parcial. El barbero dormilón es el grupo de hilos de meteo-api: trabajadores que duermen a 0 % de CPU esperando peticiones, una cola acotada, y el rechazo explícito con 503 cuando se llena —porque rechazar carga es una decisión de diseño, no un fallo, y una cola grande no aumenta la capacidad, solo la latencia—.
Queda una deuda concreta. En los filósofos hemos visto el ciclo de esperas y lo hemos esquivado con tres trucos, pero no hemos explicado por qué funcionan ni qué tienen en común. ¿Qué condiciones exactas deben darse a la vez para que un interbloqueo sea posible? ¿Se puede detectar uno ya producido, o predecirlo antes de conceder un recurso? ¿Y cómo se diagnostica un servicio real que se ha quedado colgado a las tres de la mañana, cuando no hay ningún filósofo a la vista sino un agregador y un meteo-api que no responden?
Lo cerramos en Interbloqueos: Prevención, Detección y Recuperación.
Fundamentos de Sistemas Operativos
Módulo 1: Introducción a los Sistemas Operativos
- Conceptos Básicos de Sistemas Operativos
- Historia y Evolución de los Sistemas Operativos
- Tipos de Sistemas Operativos
- Funciones Principales de un Sistema Operativo
- Arquitectura del Núcleo: Monolítico, Microkernel e Híbrido
- Modo Usuario, Modo Núcleo y Llamadas al Sistema
Módulo 2: Gestión de Recursos
- Gestión de Procesos
- Planificación de la CPU
- Gestión de Memoria
- Memoria Virtual y Paginación
- Gestión de Almacenamiento
- Gestión de Dispositivos
- Controladores, Interrupciones y Operaciones de E/S
Módulo 3: Concurrencia
- Conceptos de Concurrencia
- Hilos y Procesos
- Comunicación entre Procesos (IPC)
- Sincronización y Exclusión Mutua
- Problemas Clásicos de Concurrencia
- Interbloqueos: Prevención, Detección y Recuperación
Módulo 4: Estructuras de Archivos
- Sistemas de Archivos
- Estructuras de Directorios
- Particiones, Montaje y Sistema de Archivos Virtual
- Gestión de Archivos
- Asignación de Espacio, Journaling e Integridad
- Seguridad y Permisos de Archivos
Módulo 5: Protección y Seguridad del Sistema
- Principios de Protección y Control de Acceso
- Usuarios, Autenticación y Escalada de Privilegios
- Amenazas Comunes y Endurecimiento del Sistema
- Auditoría, Registros y Respuesta a Incidentes
Módulo 6: Virtualización y Contenedores
- Virtualización: Hipervisores y Máquinas Virtuales
- Contenedores: Namespaces y cgroups
- El Sistema Operativo en la Nube
- Sistemas Operativos Móviles y de Tiempo Real
