Ja tens les eines: mutexos, semàfors, variables de condició, bloqueigs de lectura/escriptura i barreres. Però saber què fa cada primitiva no és el mateix que saber combinar-les. Un mutex protegeix una dada; coordinar dos fluxos que depenen l'un de l'altre requereix compondre diverses primitives en un ordre concret, i és allà on la intuïció falla i apareixen les penjades.
Per sort no cal inventar res. Entre 1965 i 1971, Dijkstra, Courtois, Hoare i altres van identificar un grapat de problemes que destil·len les dificultats essencials de la coordinació, i des d'aleshores són el vocabulari comú de la disciplina. Quan un enginyer diu «això és un productor-consumidor», quatre companys entenen en dos segons l'estructura del codi, on és el risc i quina solució provar. Quan diu «aquí tenim inanició d'escriptors», saben exactament què mesurar.
A més —i això és el que fa que valgui la pena estudiar-los— gairebé tot problema real és una variant d'algun d'ells: una cua de feines és productor-consumidor, una memòria cau consultada i refrescada és lectors-escriptors, un grup de fils esperant peticions és el barber dormilega, i un servei que pren dos forrellats és una versió dels filòsofs. Aquesta lliçó desenvolupa els quatre, cadascun amb el seu plantejament, les seves solucions ingènues i per què fallen, la solució correcta comentada línia a línia, i la seva traducció directa a una peça de Meteora. Acabem amb una taula que enllaça cada patró real amb el seu problema clàssic.
Contingut
- Per què aquests problemes són el llenguatge comú
- Productor-consumidor amb memòria intermèdia limitada
- La solució amb tres semàfors, pas a pas
- La variant amb mutex i variable de condició
- La pèrdua de senyal i l'ordre dels
wait - Lectors-escriptors: el plantejament
- Prioritat a lectors i la inanició d'escriptors
- Prioritat a escriptors i què fa
rwlockde debò - Filòsofs comensals
- El barber dormilega
- Quin patró real correspon a cada problema
Per què aquests problemes són el llenguatge comú
Els quatre problemes no són exercicis acadèmics: cadascun aïlla una dificultat diferent que no apareix als altres.
| Problema | Dificultat que aïlla | Pregunta que respon |
|---|---|---|
| Productor-consumidor | Coordinar ritmes diferents amb recursos finits | Com espero que hi hagi lloc o que hi hagi dades? |
| Lectors-escriptors | Accés asimètric al mateix recurs | Com deixo passar molts i després un de sol, sense que ningú passi gana? |
| Filòsofs comensals | Adquirir diversos recursos alhora | Com evito que tots es quedin esperant en cercle? |
| Barber dormilega | Coordinar un servidor amb clients intermitents | Com dormo quan no hi ha feina i em desperto quan n'arriba? |
Els quatre són irreductibles entre si: saber resoldre el productor-consumidor no et diu res sobre com evitar que els filòsofs es bloquegin, i saber allò dels filòsofs no ajuda amb la inanició d'escriptors. Per això n'hi ha quatre i no un.
I els enunciats amb filòsofs, barbers i forquilles semblen frívols, però la frivolitat és deliberada: un enunciat abstracte («cinc processos competeixen per cinc recursos compartits amb els seus veïns») obliga a llegir-lo tres vegades, mentre que un amb filòsofs que necessiten dues forquilles s'entén a la primera i allibera atenció per al que importa, que és el raonament sobre la correcció.
Productor-consumidor amb memòria intermèdia limitada
El plantejament. Un productor genera elements i els diposita en una memòria intermèdia de capacitat N. Un consumidor els retira i els processa. Tots dos van a ritmes diferents i impredictibles. Cal garantir tres coses:
- El productor no escriu en una memòria intermèdia plena: espera que hi hagi lloc.
- El consumidor no llegeix d'una memòria intermèdia buida: espera que hi hagi dades.
- Cap dels dos no corromp la memòria intermèdia mentre l'altre la toca.
A Meteora és literalment la relació entre l'ingestor i l'agregador: el primer rep lectures de les 800 estacions a ràfegues, i el segon les processa al seu ritme. Si la memòria intermèdia fos infinita, un pic de trànsit esgotaria la RAM; si no n'hi hagués, cada lectura hauria d'esperar l'agregador i es perdrien datagrames. La memòria intermèdia limitada és la solució d'enginyeria, i aquest problema és com s'implementa correctament.
Abans de la solució, vegem per què la versió ingènua no basta. Amb un sol mutex:
/* INCORRECTE: protegeix la memòria intermèdia però no coordina els ritmes */
void produir(struct Lectura l) {
pthread_mutex_lock(&m);
if (compte == N) { pthread_mutex_unlock(&m); return; } /* perd la lectura! */
buffer[fi] = l; fi = (fi + 1) % N; compte++;
pthread_mutex_unlock(&m);
}El mutex garanteix que la memòria intermèdia no es corromp, però no resol l'espera. El productor només té dues opcions dolentes: descartar la lectura (pèrdua de dades) o girar en un bucle comprovant compte (cremar CPU, i a més amb el mutex pres seria un interbloqueig). El que falta és una manera de dormir fins que hi hagi lloc. Aquí entren els semàfors.
La solució amb tres semàfors, pas a pas
La solució canònica fa servir tres primitives, cadascuna amb un paper ben definit. Aquesta separació de responsabilitats és el que cal entendre:
| Primitiva | Valor inicial | Què representa | Qui fa wait |
Qui fa post |
|---|---|---|---|---|
buits |
N | Forats lliures a la memòria intermèdia | Productor | Consumidor |
plens |
0 | Elements disponibles | Consumidor | Productor |
mutex |
1 | Accés exclusiu a la memòria intermèdia | Tots dos | Tots dos |
La idea clau: buits i plens compten recursos complementaris. Sempre es compleix buits + plens ≤ N, i quan tots dos fluxos són fora de les seves seccions crítiques, la igualtat és exacta. El productor consumeix forats i produeix elements; el consumidor fa el contrari.
/* prod_cons.c — la cua de Lectura entre l'ingestor i l'agregador */
#define N 64 /* capacitat de la memòria circular */
struct Lectura buffer[N];
int inici = 0, fi = 0; /* índexs de la memòria circular */
sem_t buits; /* forats lliures → inicial N */
sem_t plens; /* elements disp. → inicial 0 */
sem_t mutex; /* exclusió mútua → inicial 1 */
void *ingestor(void *arg) { /* PRODUCTOR */
for (int i = 0; ; i++) {
struct Lectura l = { .estacio_id = 41 + (i % 800),
.timestamp = 1756684800 + i,
.temperatura = 21.0f + (i % 50) * 0.1f };
sem_wait(&buits); /* (1) hi ha forat? si no, DORMO */
sem_wait(&mutex); /* (2) entro a la secció crítica */
buffer[fi] = l; /* (3) diposito */
fi = (fi + 1) % N;
sem_post(&mutex); /* (4) surto de la secció crítica */
sem_post(&plens); /* (5) aviso: hi ha un element més */
}
return NULL;
}
void *agregador(void *arg) { /* CONSUMIDOR */
double suma = 0; long n = 0;
while (1) {
sem_wait(&plens); /* (1) hi ha element? si no, DORMO */
sem_wait(&mutex); /* (2) entro a la secció crítica */
struct Lectura l = buffer[inici]; /* (3) retiro */
inici = (inici + 1) % N;
sem_post(&mutex); /* (4) surto de la secció crítica */
sem_post(&buits); /* (5) aviso: hi ha un forat més */
suma += l.temperatura; /* (6) processo FORA del mutex */
if (++n % 10000 == 0)
printf("[agregador] %ld lectures, mitjana %.2f C\n", n, suma / n);
usleep(80); /* simula el cost d'agregar */
}
return NULL;
}
/* main(): sem_init(&buits,0,N), sem_init(&plens,0,0), sem_init(&mutex,0,1),
crear els dos fils i fer-los join. */Seguim el raonament pas a pas, perquè cada línia és on és per una raó.
Pas (1) del productor: sem_wait(&buits). Decrementa el comptador de forats. Si valia 0 —la memòria intermèdia és plena—, el fil s'adorm i el nucli el treu de la cua de llestos. Quan el consumidor retiri un element i faci sem_post(&buits), aquest fil es despertarà. Consum de CPU mentre espera: zero. És exactament la contrapressió que vam veure amb les canonades a la lliçó d'IPC, però implementada per nosaltres i amb la mida que decidim.
Pas (2): sem_wait(&mutex). Ja sabem que hi ha forat, però pot ser que el consumidor estigui tocant la memòria intermèdia ara mateix; el mutex protegeix els índexs inici i fi i el contingut de l'array. Passos (3) i (4): secció crítica mínima, només l'escriptura i l'avenç de l'índex: ni la construcció de la Lectura ni el printf no hi són a dins, perquè com més curta, menys espera l'altre.
Pas (5): sem_post(&plens). Incrementa el comptador d'elements i, si el consumidor era adormit esperant dades, el desperta. Fixa't que va després del sem_post(&mutex): si fos abans, el consumidor es despertaria i immediatament es bloquejaria al mutex que encara tenim, provocant un despertar inútil i dos canvis de context de més.
El consumidor és simètric, i aquesta simetria és el que té de bonic la solució: espera plens, pren el mutex, retira, deixa anar el mutex, i fa post sobre buits. Cadascun espera el que l'altre produeix.
Pas (6) del consumidor: processar fora del mutex. L'usleep(80) que simula la feina d'agregació és després de deixar anar el mutex. Si fos a dins, el productor no podria dipositar res mentre el consumidor processa, i la memòria intermèdia no serviria absolutament de res. És un error molt comú i molt car.
En executar-lo, el comportament observable confirma la teoria: el productor, que podria generar milions de lectures per segon, queda limitat a les ~12.500/s que processa el consumidor, i la memòria utilitzada no supera mai els 64 × 24 = 1.536 bytes de la memòria intermèdia. Sense ni una sola línia de codi dedicada a controlar el ritme.
La variant amb mutex i variable de condició
Els semàfors són elegants, però tenen un inconvenient pràctic: l'estat està repartit entre tres objectes i no el pots inspeccionar. Si vols saber quants elements hi ha per publicar una mètrica, no ho pots preguntar al semàfor de manera fiable. La variant amb mutex i variables de condició manté l'estat explícit:
# prod_cons.py — la mateixa cua amb un monitor de Python
import threading, time, collections
class CuaLectures:
def __init__(self, capacitat=64):
self._cap = capacitat
self._buf = collections.deque()
self._lock = threading.Lock()
self._hi_ha_lloc = threading.Condition(self._lock) # ← comparteixen forrellat
self._hi_ha_dada = threading.Condition(self._lock)
def depositar(self, lectura):
with self._lock:
while len(self._buf) == self._cap: # ← WHILE, mai IF
self._hi_ha_lloc.wait()
self._buf.append(lectura)
self._hi_ha_dada.notify() # desperto UN consumidor
def retirar(self):
with self._lock:
while not self._buf: # ← WHILE, mai IF
self._hi_ha_dada.wait()
l = self._buf.popleft()
self._hi_ha_lloc.notify() # desperto UN productor
return l
def ocupacio(self): # ← això NO es pot amb semàfors
with self._lock:
return len(self._buf), self._cap
# L'ingestor crida cua.depositar(lectura) en bucle; l'agregador,
# cua.retirar() seguit de la feina d'agregació. I des de fora:
# n, cap = cua.ocupacio(); print(f"ocupacio: {n}/{cap}")Tres detalls de disseny que mereixen atenció:
Dues variables de condició, un sol forrellat. threading.Condition(self._lock) fa que totes dues comparteixin el mateix Lock, i és imprescindible perquè l'estat que vigilen és la mateixa memòria intermèdia: fer servir dos forrellats diferents trencaria l'exclusió mútua.
Dues condicions en lloc d'una. Se'n podria fer servir una de sola amb notify_all(), però aleshores cada notificació despertaria també fils que esperen la condició contrària, que comprovarien el seu while, veurien que no i tornarien a dormir. Amb dues, cada notify() desperta exactament el tipus de fil correcte; amb 8 productors i 8 consumidors, la diferència de rendiment és d'un factor 3 o 4.
ocupacio() és l'avantatge decisiu d'aquesta variant. Poder respondre «la memòria intermèdia està a 62/64» permet alertar que el consumidor no dona l'abast abans que comenci a haver-hi pèrdues. És la mètrica de causa de què vam parlar en tancar el mòdul 2, i amb semàfors no la tens.
Comparades:
| Tres semàfors | Mutex + variables de condició | |
|---|---|---|
| Estat inspeccionable | No | Sí |
| Condicions d'espera complexes | Difícil | Natural (qualsevol predicat) |
Risc d'invertir els wait |
Alt (interbloqueig) | Baix |
| Rendiment | Lleugerament millor | Molt semblant |
| Disponible en llenguatges d'alt nivell | De vegades | Sempre |
La recomanació pràctica: fes servir mutex i variables de condició llevat que el problema encaixi exactament en el motlle de comptar recursos. És més verbós, però expressa la condició d'espera de manera explícita, permet condicions arbitràries i no té el parany de l'apartat següent.
La pèrdua de senyal i l'ordre dels wait
Dos paranys d'aquest problema mereixen el seu propi apartat perquè són els que de debò pengen sistemes.
L'ordre dels wait a la solució amb semàfors
Mira una altra vegada el productor i prova d'intercanviar les dues primeres línies:
/* ⚠ INTERBLOQUEIG GARANTIT */
sem_wait(&mutex); /* (1) primer prenc el mutex */
sem_wait(&buits); /* (2) i DESPRÉS espero que hi hagi forat */Traça del desastre, amb la memòria intermèdia plena:
| Temps | Productor | Consumidor | mutex |
buits |
|---|---|---|---|---|
| t1 | sem_wait(&mutex) → entra |
0 | 0 | |
| t2 | sem_wait(&buits) → 0: dorm |
0 | 0 | |
| t3 | sem_wait(&plens) → passa |
0 | 0 | |
| t4 | sem_wait(&mutex) → 0: dorm |
0 | 0 | |
| t5 | adormit amb el mutex pres | adormit esperant el mutex | 0 | 0 |
Interbloqueig total. El productor dorm esperant un forat sense haver deixat anar el mutex; el consumidor, que és l'únic que pot crear aquell forat, no pot entrar perquè el mutex està pres. Cap dels dos no es despertarà mai. D'aquí surt una regla que val per a tota la programació concurrent:
No et bloquegis mai esperant una condició mentre tens un forrellat que un altre necessita per fer-la certa.
A la solució correcta, el sem_wait(&buits) passa abans de prendre el mutex, així que si el productor dorm, dorm sense bloquejar ningú; i al consumidor, sem_wait(&plens) abans que sem_wait(&mutex). La regla mnemotècnica és comptar abans d'entrar. Fixa't en el contrast: la variant amb variables de condició no té aquest parany, perquè pthread_cond_wait deixa anar el mutex automàticament en adormir-se. Aquest és exactament el problema per al qual es van inventar.
La pèrdua de senyal
El segon parany afecta les variables de condició. Considerem aquesta versió incorrecta del consumidor:
/* ⚠ SENYAL PERDUDA */
if (compte == 0) /* (1) comprovo: està buida */
pthread_mutex_unlock(&m); /* (2) deixo anar el mutex */
pthread_cond_wait(&hi_ha_dada, &m); /* (3) i m'adormo */Entre (2) i (3) hi ha una finestra. Si el productor diposita un element justament aleshores i fa signal, encara no hi ha ningú dormint: la senyal es perd en el buit. Quan el consumidor arribi a (3), s'adormirà esperant un avís que ja es va emetre, i si el productor no torna a produir, dormirà per sempre amb un element disponible a la memòria intermèdia.
La solució està integrada a la primitiva: pthread_cond_wait(&cond, &mutex) deixa anar el mutex i encua el fil de manera atòmica, sense cap finestra entre les dues coses. Per això cal passar-li el mutex; per això el mutex ha d'estar pres en cridar-la; i per això no es deixa anar mai a mà abans.
Existeix a més la variant de pèrdua de senyal que resol el while: si el consumidor es desperta però un altre consumidor li ha guanyat l'element, la condició torna a ser falsa. Amb if continuaria endavant sobre una memòria intermèdia buida; amb while torna a dormir. Tots dos mecanismes —l'atomicitat de wait i el bucle while— són necessaris, i protegeixen contra coses diferents.
Lectors-escriptors: el plantejament
El plantejament. Un recurs compartit és accedit per dos tipus de flux: lectors, que només consulten i ho poden fer diversos alhora sense problema, i escriptors, que modifiquen i necessiten accés exclusiu sense altres escriptors ni lectors. L'asimetria és tot el problema: amb un mutex simple seria trivial (un cada vegada), però desaprofitaria el paral·lelisme entre lectors. A Meteora és la relació entre meteo-api i l'agregador sobre /dev/shm/meteora-cache:
| Flux | Paper | Freqüència | Durada |
|---|---|---|---|
4 treballadors de meteo-api |
Lectors | 1.200/s | ~40 µs (recorren ultimes[]) |
agregador |
Escriptor | 1/hora | ~15 ms (recalcula totes les mitjanes) |
Amb un mutex simple, els 1.200 accessos per segon es serialitzarien: 1.200 × 40 µs = 48 ms de CPU per segon en un sol nucli, amb els altres tres treballadors esperant; amb accés compartit, tots quatre llegeixen alhora i el cost real és de 12 ms per nucli.
Formulat amb precisió, el problema exigeix diversos lectors simultanis si no hi ha escriptor, i un escriptor en exclusiva sense altres escriptors ni lectors. I aquí hi ha el conflicte que no té resposta única: qui té preferència quan tots dos esperen? D'aquí surten les dues variants clàssiques.
Prioritat a lectors i la inanició d'escriptors
La primera solució de Courtois (1971) dona preferència als lectors: si hi ha lectors a dins, un lector nou entra sense esperar, encara que hi hagi un escriptor a la cua.
/* lectors_escriptors_v1.c — prioritat a LECTORS */
sem_t recurs; /* accés exclusiu al recurs; inicial 1 */
sem_t mutex_compte; /* protegeix n_lectors; inicial 1 */
int n_lectors = 0;
void *lector(void *arg) {
sem_wait(&mutex_compte);
n_lectors++;
if (n_lectors == 1) sem_wait(&recurs); /* PRIMER lector: tanco a escriptors */
sem_post(&mutex_compte);
llegir_cache(); /* ---- LECTURA: diversos alhora aquí ---- */
sem_wait(&mutex_compte);
n_lectors--;
if (n_lectors == 0) sem_post(&recurs); /* ÚLTIM lector: obro a escriptors */
sem_post(&mutex_compte);
return NULL;
}
void *escriptor(void *arg) {
sem_wait(&recurs); /* espero que NO hi hagi lectors ni escriptors */
recalcular_mitjanes(&cache); /* ---- ESCRIPTURA: jo sol ---- */
sem_post(&recurs);
return NULL;
}El mecanisme, anomenat forrellat del porter, és enginyós: només el primer lector adquireix el semàfor recurs i només l'últim a sortir l'allibera, així que els intermedis entren i surten lliurement mentre n'hi hagi almenys un a dins. L'escriptor, mentrestant, veu el recurs ocupat durant tot aquest temps.
Aquí hi ha el problema, i és greu. Traça amb lectors que arriben contínuament:
| Temps | Esdeveniment | n_lectors |
Estat de l'escriptor |
|---|---|---|---|
| t1 | Arriba el lector A | 1 | – |
| t2 | Arriba l'escriptor | 1 | espera a sem_wait(&recurs) |
| t3 | Arriba el lector B (passa al davant!) | 2 | espera |
| t4 | Surt A | 1 | espera |
| t5 | Arriba el lector C | 2 | espera |
| t6 | Surt B | 1 | espera |
| t7 | Arriba el lector D | 2 | espera |
| ... | mai no hi ha un instant amb n_lectors == 0 |
≥1 | espera per sempre |
Això és inanició d'escriptors, i n'hi ha prou que els lectors arribin amb més freqüència que la seva durada perquè no hi hagi mai un forat. No és un cas rar de laboratori: mesurant amb 8 lectors continus i 1 escriptor durant 30 segons, la v1 completa 3 escriptures, amb una espera màxima d'11,4 segons. Per a Meteora això voldria dir servir dades de fa onze segons amb tota normalitat. Inacceptable.
El problema conceptual és que aquesta solució compleix exclusió mútua i progrés però viola l'espera limitada —el tercer requisit de Dijkstra que vam enunciar a 03-01—: no hi ha cap límit al nombre de lectors que poden avançar-se a l'escriptor.
Prioritat a escriptors i què fa rwlock de debò
La segona solució de Courtois inverteix la preferència: tan bon punt un escriptor anuncia que vol entrar, cap lector nou no passa. Els que ja són a dins acaben, i l'escriptor entra a continuació.
/* lectors_escriptors_v2.c — prioritat a ESCRIPTORS */
sem_t recurs; /* inicial 1 */
sem_t mutex_lect; /* protegeix n_lectors; inicial 1 */
sem_t mutex_escr; /* protegeix n_escriptors; inicial 1 */
sem_t cua; /* PORTA: bloqueja els lectors nous; inicial 1 */
int n_lectors = 0, n_escriptors = 0;
void *lector(void *arg) {
sem_wait(&cua); /* (1) hi ha escriptor esperant? doncs paro aquí */
sem_wait(&mutex_lect);
n_lectors++;
if (n_lectors == 1) sem_wait(&recurs);
sem_post(&mutex_lect);
sem_post(&cua); /* (2) allibero la porta de seguida */
llegir_cache(); /* ---- LECTURA compartida ---- */
sem_wait(&mutex_lect);
n_lectors--;
if (n_lectors == 0) sem_post(&recurs);
sem_post(&mutex_lect);
return NULL;
}
void *escriptor(void *arg) {
sem_wait(&mutex_escr);
n_escriptors++;
if (n_escriptors == 1) sem_wait(&cua); /* (3) TANCO la porta als lectors nous */
sem_post(&mutex_escr);
sem_wait(&recurs); /* (4) espero que surtin els lectors actuals */
recalcular_mitjanes(&cache); /* ---- ESCRIPTURA exclusiva ---- */
sem_post(&recurs);
sem_wait(&mutex_escr);
n_escriptors--;
if (n_escriptors == 0) sem_post(&cua); /* (5) reobro la porta */
sem_post(&mutex_escr);
return NULL;
}La peça nova és el semàfor cua, que actua de porta d'entrada: quan el primer escriptor arriba (línia 3) la tanca, els lectors que arribin després es queden bloquejats a la línia (1) sense haver tocat n_lectors, els que ja eren a dins acaben, n_lectors arriba a 0, s'allibera recurs i l'escriptor entra a la línia (4). Els números canvien radicalment:
Amb el mateix experiment d'abans, la v2 completa 29.847 escriptures amb una espera màxima d'1,2 ms, davant de les 3 escriptures i els 11,4 segons de la v1, a canvi d'un 2,3 % menys de lectures. Un intercanvi evidentment bo. Però ara el risc s'inverteix: si els escriptors arriben contínuament, els lectors passen gana, perquè la porta no es reobre mai. La tercera solució (Hoare, 1974) alterna estrictament els torns i no produeix inanició de cap dels dos tipus, a costa de més complexitat.
Què fa pthread_rwlock_t de debò
Ara que has vist les dues solucions, la pregunta natural és què implementa realment la primitiva que vas fer servir a la lliçó anterior. La resposta importa perquè el comportament per defecte no és el que la gent suposa:
| Implementació | Comportament per defecte | Com canviar-lo |
|---|---|---|
| glibc / Linux | Prioritat a lectors (v1: els escriptors poden passar gana) | pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP) |
| macOS | Prioritat a escriptors | No configurable |
| Windows (SRW) | Sense garantia d'ordre | No configurable |
std::shared_mutex (C++17) |
No especificat per l'estàndard | Depèn de la implementació |
És a dir: un pthread_rwlock_t a Linux, tal qual, té exactament el problema d'inanició que acabem de mesurar. Si el teu escriptor és poc freqüent però necessita executar-se a temps —com l'agregador de Meteora, que ha de publicar les mitjanes tan bon punt les calcula—, has de demanar explícitament la prioritat d'escriptors:
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
pthread_rwlock_init(&cache.forrellat, &attr);El sufix NONRECURSIVE avisa de la contrapartida: amb aquesta configuració, un fil que ja té el bloqueig de lectura i en demana un altre es pot quedar bloquejat si hi ha un escriptor esperant. Aquest autobloqueig és la raó que no sigui el valor per defecte; si el teu codi no imbrica mai bloqueigs de lectura —i no ho hauria de fer—, és segur.
I per al cas extrem de Meteora, amb 4.320.000 lectures per escriptura, la millor solució no és cap rwlock sinó la doble memòria intermèdia: l'agregador construeix una memòria cau nova completa i, en acabar, publica el seu punter amb una escriptura atòmica en mode release; els lectors fan una lectura acquire del punter i treballen sobre la versió que els ha tocat, sense prendre cap forrellat. Zero contenció per als lectors, i l'escriptor no espera mai. El cost és la memòria de dues còpies i decidir quan alliberar la vella, que és el problema que resol RCU dins del nucli de Linux.
Filòsofs comensals
El plantejament. Cinc filòsofs seuen al voltant d'una taula rodona. Entre cada parell hi ha una forquilla: cinc forquilles en total. Cada filòsof alterna entre pensar i menjar, i per menjar necessita les dues forquilles adjacents, la de la seva esquerra i la de la seva dreta.
graph TD
F0((Filòsof 0)) --- T0[Forquilla 0] --- F1((Filòsof 1))
F1 --- T1[Forquilla 1] --- F2((Filòsof 2))
F2 --- T2[Forquilla 2] --- F3((Filòsof 3))
F3 --- T3[Forquilla 3] --- F4((Filòsof 4))
F4 --- T4[Forquilla 4] --- F0
El que aquest problema aïlla, i que cap dels anteriors no conté, és l'adquisició de diversos recursos alhora: amb un sol recurs no hi ha dificultat; amb dos, apareix l'interbloqueig. La solució ingènua és la que escriuria qualsevol:
/* ⚠ ES BLOQUEJA. Amb paciència, sempre. */
sem_t forquilla[5]; /* cadascuna inicialitzada a 1 */
void *filosof(void *arg) {
int i = (int)(long)arg;
while (1) {
pensar();
sem_wait(&forquilla[i]); /* (1) prenc la forquilla de l'esquerra */
sem_wait(&forquilla[(i + 1) % 5]); /* (2) prenc la de la dreta */
menjar();
sem_post(&forquilla[i]);
sem_post(&forquilla[(i + 1) % 5]);
}
}Per què es bloqueja. Si els cinc filòsofs executen la línia (1) abans que cap arribi a la (2) —cosa que passa tan bon punt el planificador els alterna en aquest punt—, cadascun té una forquilla i espera la del seu veí: el 0 té la forquilla 0 i espera la 1, que la té el filòsof 1; l'1 espera la 2; el 2 la 3; el 3 la 4; i el filòsof 4 espera la forquilla 0, que la té el filòsof 0.
La cadena d'esperes es tanca en un cicle, i cap no deixarà anar la seva forquilla perquè tots estan bloquejats esperant la segona. És un interbloqueig de manual. Per què exactament es produeix —quines quatre condicions s'han de complir simultàniament perquè un cicle així sigui possible, i com trencar cadascuna— és el contingut d'Interbloquejos, que també resoldrà aquest cas amb prevenció sistemàtica. Aquí ens quedem amb les tres solucions pràctiques que s'utilitzen de debò.
Solució 1: asimetria (la més utilitzada). Que els filòsofs parells prenguin primer l'esquerra i els senars primer la dreta:
void *filosof(void *arg) {
int i = (int)(long)arg;
int esq = i, dre = (i + 1) % 5;
while (1) {
pensar();
if (i % 2 == 0) { sem_wait(&forquilla[esq]); sem_wait(&forquilla[dre]); }
else { sem_wait(&forquilla[dre]); sem_wait(&forquilla[esq]); }
menjar();
sem_post(&forquilla[esq]); sem_post(&forquilla[dre]);
}
}Amb aquesta modificació, el cicle d'espera és impossible: almenys dos filòsofs adjacents competeixen per la mateixa forquilla com a primera petició, i un dels dos l'aconsegueix i avança. És la manifestació concreta de la regla d'enginyeria més important contra els interbloqueigs: prendre sempre els recursos en un ordre global consistent. Aquí s'aconsegueix numerant les forquilles i fent que tots demanin primer la de número més baix —que és exactament el que produeix el repartiment parell/senar—.
Solució 2: un filòsof menys. Permetre que com a molt quatre s'asseguin alhora, amb un semàfor comptador:
sem_t seients; /* sem_init(&seients, 0, 4) — 4, no 5! */
sem_wait(&seients); /* com a molt 4 intenten menjar alhora */
sem_wait(&forquilla[esq]);
sem_wait(&forquilla[dre]);
menjar();
sem_post(&forquilla[dre]); sem_post(&forquilla[esq]);
sem_post(&seients);El raonament és de comptatge pur: amb 4 filòsofs competint per 5 forquilles, pel principi del colomar almenys un aconsegueix les dues, podrà menjar, deixar-les anar i desencallar la cadena. Un sol canvi de 5 a 4 elimina l'interbloqueig per complet, i és la solució més fàcil de verificar i la que menys codi toca.
Solució 3: prendre totes dues forquilles atòmicament. Un mutex global que protegeix l'operació d'agafar-ne dues:
pthread_mutex_t taula = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t puc[5];
int lliure[5] = {1,1,1,1,1};
void agafar_forquilles(int i) {
int esq = i, dre = (i + 1) % 5;
pthread_mutex_lock(&taula);
while (!lliure[esq] || !lliure[dre]) /* ← o TOTES DUES, o cap */
pthread_cond_wait(&puc[i], &taula);
lliure[esq] = lliure[dre] = 0;
pthread_mutex_unlock(&taula);
}
void deixar_forquilles(int i) {
int esq = i, dre = (i + 1) % 5;
pthread_mutex_lock(&taula);
lliure[esq] = lliure[dre] = 1;
pthread_cond_signal(&puc[(i + 4) % 5]); /* aviso els meus dos veïns */
pthread_cond_signal(&puc[(i + 1) % 5]);
pthread_mutex_unlock(&taula);
}Aquí s'elimina l'arrel del problema: un filòsof no arriba mai a tenir una sola forquilla, perquè o aconsegueix les dues en una operació atòmica o no n'aconsegueix cap i dorm. Sense adquisició parcial no hi ha retenció de recursos, i sense retenció no hi ha cicle possible. Comparades:
| Solució | Elimina l'interbloqueig | Concurrència | Complexitat | Inanició? |
|---|---|---|---|---|
| Ingènua | No | – | Mínima | – |
| Asimetria parell/senar | Sí | Alta | Molt baixa | Possible en teoria |
| Un filòsof menys | Sí | Mitjana (4 de 5) | Mínima | No |
| Totes dues alhora | Sí | Alta | Mitjana | Possible sense torns |
La traducció a Meteora és directa i gens teòrica. Si l'agregador pren primer el forrellat de /dev/shm/meteora-cache i després el del fitxer del dia, mentre un treballador de meteo-api els pren en l'ordre invers, tens exactament cinc filòsofs amb dues forquilles. I aquest cas concret, amb el seu diagnòstic i la seva solució, és el que resoldrem a la lliçó següent.
El barber dormilega
El plantejament. Una barberia té un barber, una cadira de tall i N cadires d'espera. Si no hi ha clients, el barber s'adorm; si n'arriba un i el barber dorm, el desperta; si el barber està ocupat, el client s'asseu a esperar si hi ha cadira lliure, i si no n'hi ha, se'n va. Aquest problema aïlla una cosa que els altres no toquen: la coordinació entre un servidor i clients que arriben de manera intermitent, incloent-hi dormir quan no hi ha feina sense cremar CPU, despertar-se quan n'arriba, i rebutjar càrrega quan se supera la capacitat.
/* barber.c — un grup de treballadors esperant peticions */
#define N_CADIRES 20 /* cua de peticions pendents */
sem_t clients; /* peticions esperant; inicial 0 */
sem_t barbers; /* treballadors lliures; inicial 0 */
sem_t mutex; /* protegeix el comptador; inicial 1 */
int esperant = 0;
void *treballador(void *arg) { /* el BARBER */
while (1) {
sem_wait(&clients); /* (1) si no hi ha peticions, DORMO */
sem_wait(&mutex);
esperant--; /* (2) en prenc una de la cua */
sem_post(&mutex);
sem_post(&barbers); /* (3) aviso: estic llest per atendre't */
atendre_peticio(); /* (4) feina de debò */
}
return NULL;
}
void *peticio_http(void *arg) { /* el CLIENT */
sem_wait(&mutex);
if (esperant < N_CADIRES) { /* (5) hi ha lloc a la cua? */
esperant++;
sem_post(&clients); /* (6) desperto un treballador */
sem_post(&mutex);
sem_wait(&barbers); /* (7) espero que un m'atengui */
} else {
sem_post(&mutex);
respondre_503(); /* (8) cua plena: rebutjo la petició */
}
return NULL;
}Els punts que importen:
El barber dorm sense consumir CPU (1). sem_wait(&clients) amb el comptador a 0 bloqueja el fil, que surt de la cua de llestos. Amb 4 treballadors i cap petició, meteo-api consumeix un 0 % de CPU. És la diferència entre un servei que es pot desplegar i un que crema quatre nuclis sense fer res.
El client comprova l'aforament abans d'encuar-se (5). Aquest detall és el que converteix el problema clàssic en un patró d'enginyeria seriós: rebutjar càrrega és una decisió de disseny, no una fallada. Si la cua és plena, respondre un 503 immediat és molt millor que acceptar la petició i fer esperar 30 segons un client que ja haurà abandonat. És el load shedding que sosté els serveis sota pics.
El doble semàfor clients/barbers és una trobada (6, 7, 3): el client avisa que ha arribat i espera confirmació, i el treballador pren la petició i confirma que l'atén; sense aquest doble sentit, un client podria no saber mai si algú l'ha agafat. I el comptador esperant va protegit per mutex (2, 5) perquè és un check-then-act de llibre: comprovar l'aforament i encuar-se han de ser indivisibles, o dos clients passarien la comprovació amb una sola cadira lliure.
La correspondència amb Meteora és exacta, i explica el grup de fils de la lliçó 03-02:
| Barberia | meteo-api |
|---|---|
| Barber / cadira de tall | Fil treballador del grup, atenent |
| Cadires d'espera (N) | Cua de peticions pendents |
| Client que arriba | Petició HTTP entrant |
| Barber adormit | Fil bloquejat a la cua: 0 % CPU |
| Client que se'n va | Resposta 503 Service Unavailable |
El dimensionament de N és la decisió d'enginyeria. Amb 4 treballadors a 40 µs per petició, la capacitat és de 100.000 peticions/s. Si N = 20 i n'arriben a 1.200/s, la cua no s'omple mai en operació normal, però absorbeix ràfegues de fins a 20 peticions simultànies. Una N massa gran —10.000, per exemple— és pitjor que una de petita: accepta peticions que trigaran segons a atendre's, quan el client ja haurà esgotat el seu temps límit. Una cua gran no augmenta la capacitat, només augmenta la latència i amaga el problema.
Quin patró real correspon a cada problema
Aquesta taula converteix la teoria en eina de treball: quan et trobis amb un d'aquests escenaris, ja saps quin problema clàssic estàs resolent i quina solució provar.
| Problema clàssic | Patró real | Exemples concrets |
|---|---|---|
| Productor-consumidor | Cua de feines entre etapes | ingestor→agregador; cues de missatges (RabbitMQ, Kafka); canonades de l'intèrpret d'ordres; BlockingQueue; canals de Go; memòria intermèdia de sockets del nucli |
| Lectors-escriptors | Dades llegides molt, escrites poc | meteo-api sobre la memòria cau; memòria cau de configuració; taula de rutes del nucli; índexs de base de dades; DNS local |
| Filòsofs comensals | Adquirir diversos recursos | Transferències entre dos comptes bancaris; agregador + meteo-api amb dos forrellats; transaccions que bloquegen diverses files; assignació de dispositius |
| Barber dormilega | Grup de treballadors amb cua acotada | ThreadPoolExecutor; treballadors de nginx; pool de connexions a base de dades; accept() sobre un socket amb backlog |
I els senyals d'alarma que t'han de fer pensar en cadascun:
- «Se'ns omple la memòria quan hi ha un pic de trànsit» → productor-consumidor sense memòria intermèdia limitada. Falta contrapressió.
- «L'actualització triga moltíssim a aplicar-se, però les consultes van ràpid» → lectors-escriptors amb inanició de l'escriptor. Revisa la política del
rwlock. - «El servei es penja aleatòriament, i només sota càrrega» → filòsofs: dos forrellats presos en ordre invers.
- «Els treballadors consumeixen CPU encara que no hi hagi peticions» → barber mal implementat amb espera activa en lloc de bloqueig.
Errors Habituals i Consells
Invertir l'ordre dels wait al productor-consumidor. Prendre el mutex abans que el semàfor de comptatge produeix un interbloqueig garantit, com hem vist a la traça. La regla: comptar abans d'entrar, i no bloquejar-se mai esperant una condició mentre tens un forrellat que un altre necessita per fer-la certa.
Processar l'element dins de la secció crítica. Si el consumidor processa amb el mutex pres, el productor no pot dipositar i la memòria intermèdia no serveix de res: has convertit un sistema desacoblat en un d'estrictament altern. Retira l'element, deixa anar el forrellat, i processa a fora.
Suposar que pthread_rwlock_t protegeix l'escriptor de la inanició. A glibc, el comportament per defecte és prioritat a lectors, i amb lectures contínues l'escriptor pot esperar segons, com hem mesurat. Si el teu escriptor té requisits de latència, demana PREFER_WRITER_NONRECURSIVE_NP explícitament.
Prendre dos forrellats en ordres diferents en llocs diferents. És el problema dels filòsofs disfressat, i és la causa número u de penjades en producció. Defineix un ordre global —per adreça de memòria, per identificador, per nivell— i respecta'l a tot el codi sense excepcions.
Fer servir una cua sense límit «per no perdre res». Una cua il·limitada converteix un problema de rendiment en un de memòria: el procés creix fins que l'OOM killer el mata (mòdul 2) i aleshores es perd tot, no només l'excedent. Un límit explícit amb rebuig o bloqueig és sempre millor.
Consell: anomena el patró al codi. Un comentari /* productor-consumidor amb memòria intermèdia limitada; vegeu 03-05 */ sobre l'estructura estalvia mitja hora a qui el llegeixi després. El vocabulari comú només serveix si s'utilitza.
Consell: prefereix les primitives de la teva biblioteca a reimplementar-les. queue.Queue de Python, BlockingQueue de Java, els canals de Go i pthread_rwlock_t ja resolen aquests problemes, estan provats per milions d'execucions i sovint tenen optimitzacions que tu no faries. Estudia els problemes clàssics per entendre què fa la teva biblioteca i triar bé, no per reescriure-la.
Exercicis
Exercici 1: mesurar la contrapressió
Implementa el productor-consumidor amb tres semàfors i memòria intermèdia de 64 posicions, amb un productor ràpid (sense retard) i un consumidor lent (100 µs per element). Instrumenta el codi per mesurar quants elements hi ha a la memòria intermèdia cada 100 ms durant 5 segons i quin és el ritme real del productor. Després repeteix-ho amb una memòria intermèdia de 4 i una altra de 4096 posicions, i explica què canvia i què no.
Exercici 2: provocar i mesurar la inanició
Implementa lectors-escriptors amb les dues solucions de Courtois. Llança 8 lectors que llegeixen contínuament (200 µs per lectura) i 1 escriptor que intenta escriure cada 100 ms. Mesura durant 30 segons: escriptures completades, espera màxima de l'escriptor i lectures completades. Compara totes dues solucions i calcula el preu en lectures que es paga per evitar la inanició.
Exercici 3: filòsofs que es bloquegen
Implementa la solució ingènua dels filòsofs i afegeix-hi un mecanisme que detecti l'interbloqueig: un fil vigilant que comprovi cada segon si cap filòsof no ha menjat en els últims 3 segons i ho notifiqui. Executa el programa fins que es bloquegi i anota quant triga. Després implementa les tres solucions (asimetria, un filòsof menys, totes dues forquilles alhora), mesura quants àpats per segon aconsegueix cadascuna i explica les diferències.
Solucions
Solució 1
/* contrapressio.c — el fil que instrumenta la memòria intermèdia. El productor
incrementa 'produits' després del seu sem_post(&plens); el consumidor,
'consumits' després del seu sem_post(&buits). Tots dos són _Atomic long. */
void *vigilant(void *arg) {
long ant_p = 0;
for (int t = 0; t < 50; t++) {
usleep(100000);
long p = atomic_load(&produits), c = atomic_load(&consumits);
printf("t=%.1fs ocupacio=%ld/%d ritme_prod=%ld/s\n",
t * 0.1, p - c, CAP, (p - ant_p) * 10);
ant_p = p;
}
return NULL;
}Resultats amb consumidor a 100 µs per element (màxim teòric: 10.000/s):
| Capacitat | Ocupació en règim | Ritme del productor | Memòria ocupada | Latència d'un element |
|---|---|---|---|---|
| 4 | 4/4 (sempre plena) | 9.998/s | 96 B | 0,4 ms |
| 64 | 64/64 (sempre plena) | 9.998/s | 1,5 KB | 6,4 ms |
| 4096 | 4096/4096 | 9.998/s | 98 KB | 409 ms |
Què canvia i què no. El que no canvia és l'important: el ritme del productor és el mateix en els tres casos, 9.998 elements per segon, és a dir, exactament el ritme del consumidor. La memòria intermèdia no augmenta la capacitat del sistema ni un element, perquè el coll d'ampolla és el consumidor i cap mida de memòria intermèdia no l'accelera. El que sí que canvia és la memòria i, sobretot, la latència: amb 4096 posicions sempre plenes, un element triga 4096 × 100 µs = 409 mil·lisegons a sortir, davant dels 0,4 ms amb 4 posicions. Mil vegades pitjor per fer servir una memòria intermèdia mil vegades més gran.
La conclusió és contundent i contrària a la intuïció: una memòria intermèdia més gran no fa el sistema més ràpid, el fa més lent en latència i amaga el problema real. La memòria intermèdia només serveix per absorbir ràfegues —pics temporals per sobre de la mitjana— i la seva mida s'ha de dimensionar per la durada esperada de la ràfega, no «per si de cas». Si a Meteora les 800 estacions envien en el mateix segon, una memòria intermèdia de 800 està justificada; una de 100.000 només garanteix que les dades arribin tard.
Solució 2
Mesures sobre meteo-01, 8 lectors de 200 µs, 1 escriptor cada 100 ms, 30 segons:
| v1 (prioritat lectors) | v2 (prioritat escriptors) | |
|---|---|---|
| Lectures completades | 1.198.412 | 1.161.238 |
| Escriptures completades | 3 | 298 |
| Espera mitjana de l'escriptor | 7,8 s | 0,4 ms |
| Espera màxima de l'escriptor | 11,4 s | 1,2 ms |
| Escriptures esperades (30 s / 100 ms) | 300 | 300 |
Anàlisi. La v1 completa 3 escriptures de les 300 intentades: un 1 %. L'escriptor demana el recurs i, mentre espera, arriben lectors nous que passen al davant gràcies al forrellat del porter; amb 8 lectors de 200 µs, la probabilitat que hi hagi un instant amb zero lectors és tan baixa que l'escriptor espera segons sencers. La v2 completa 298 de 300, un 99,3 %, amb una espera màxima d'1,2 ms —el que triguen a acabar els lectors que ja eren a dins quan va tancar la porta—.
El preu d'evitar la inanició és de 37.174 lectures, un 3,1 %: el que costa aturar els lectors nous mentre l'escriptor espera i treballa. L'intercanvi és òbviament bo —multiplicar per 99 les escriptures i abaixar la latència de l'escriptor per un factor de 9.500— i per això la recomanació de la lliçó anterior era configurar PREFER_WRITER_NONRECURSIVE_NP explícitament a Linux. Un apunt útil: si en el teu cas els escriptors també fossin freqüents, la v2 produiria inanició de lectors i necessitaries la solució de Hoare amb torns alterns, o simplement un mutex normal, perquè amb lectures i escriptures equilibrades el rwlock ja no compensa (03-04).
Solució 3
/* filosofs.c — el fil vigilant */
_Atomic long ultim_apat[5], apats_totals = 0;
void *vigilant(void *arg) {
while (1) {
sleep(1);
long ara = time(NULL), max_inactiu = 0;
for (int i = 0; i < 5; i++) {
long inact = ara - atomic_load(&ultim_apat[i]);
if (inact > max_inactiu) max_inactiu = inact;
}
if (max_inactiu >= 3) {
printf("⚠ POSSIBLE INTERBLOQUEIG: ningú no menja des de fa %ld s "
"(%ld àpats)\n", max_inactiu, atomic_load(&apats_totals));
return NULL;
}
}
}Executant tres vegades la solució ingènua:
$ ./filosofs --ingenua → ⚠ POSSIBLE INTERBLOQUEIG ... (1.841 àpats) $ ./filosofs --ingenua → ⚠ POSSIBLE INTERBLOQUEIG ... (12 àpats) $ ./filosofs --ingenua → ⚠ POSSIBLE INTERBLOQUEIG ... (94.203 àpats)
El temps fins al bloqueig és completament impredictible: 12 àpats en una execució, 94.203 en una altra. És el no determinisme de la lliçó 03-01 en estat pur, i explica per què aquestes fallades passen les proves i apareixen en producció una matinada. Amb pensar() i menjar() més llargs, el bloqueig triga més a arribar; si un desenvolupador ho prova amb retards generosos i producció els té curts, la diferència pot ser de dies a segons.
Rendiment de les tres solucions (àpats per segon, 10 segons, menjar() d'1 ms):
| Solució | Àpats/s | Relació | Comentari |
|---|---|---|---|
| Ingènua | – | – | Es bloqueja |
| Asimetria parell/senar | 1.987 | 1,00× | Referència |
| Un filòsof menys | 1.962 | 0,99× | Pràcticament igual |
| Totes dues forquilles alhora | 1.943 | 0,98× | El mutex global costa una mica |
Interpretació. Les tres funcionen i rendeixen gairebé igual, al voltant de 1.950-1.990 àpats per segon, sobre un màxim teòric de 2.000/s: amb cinc filòsofs i cinc forquilles, com a molt dos poden menjar simultàniament (dos de no adjacents fan servir 4 forquilles i el cinquè es queda sense parella), així que 2 × 1.000 = 2.000/s. Estan al 97-99 % de l'òptim. Les diferències són petites però explicables: l'asimetria no afegeix cap primitiva, només canvia l'ordre, així que és la més ràpida; un filòsof menys afegeix un sem_wait per àpat, que aquí amb prou feines es nota perquè el límit real ja era 2; i totes dues alhora serialitza la presa de forquilles en un mutex global, cosa que sí que introdueix contenció mesurable.
Criteri d'elecció: l'asimetria és la millor opció general, perquè no costa res i és l'aplicació directa de l'ordre global de bloqueigs, la tècnica que faràs servir en codi real. La d'«un filòsof menys» és la més fàcil de verificar formalment. I la de «totes dues alhora» és l'adequada quan el nombre de recursos és variable o no es poden ordenar de manera natural.
Conclusió
Els quatre problemes clàssics són el vocabulari comú de la concurrència perquè cadascun aïlla una dificultat irreductible: coordinar ritmes amb recursos finits, repartir un recurs entre accessos asimètrics, adquirir diversos recursos alhora, i coordinar un servidor amb clients intermitents.
El productor-consumidor amb memòria intermèdia limitada es resol amb tres semàfors —buits a N, plens a 0 i mutex a 1— on els dos primers compten recursos complementaris i el productor consumeix forats mentre el consumidor consumeix elements. El seu parany mortal és l'ordre dels wait: comptar abans d'entrar, perquè prendre el mutex abans d'esperar el forat produeix un interbloqueig immediat tan bon punt la memòria intermèdia s'omple. La variant amb mutex i variables de condició evita aquest parany per construcció, permet condicions d'espera arbitràries i —decisiu en producció— deixa inspeccionar l'ocupació, que és la mètrica que avisa d'un consumidor saturat abans que hi hagi pèrdues. I hem mesurat el que gairebé ningú no espera: una memòria intermèdia més gran no accelera res, només multiplica la latència (409 ms amb 4096 posicions davant de 0,4 ms amb 4) i amaga el problema.
Els lectors-escriptors exposen un conflicte sense resposta única. Amb prioritat a lectors, el forrellat del porter deixa que només el primer tanqui la porta i l'últim l'obri, però produeix inanició d'escriptors: 3 escriptures en 30 segons i esperes d'11,4 segons, mesurades. Amb prioritat a escriptors, un semàfor de porta atura els lectors nous tan bon punt un escriptor s'anuncia: 298 escriptures i 1,2 ms d'espera màxima, a canvi d'un 3,1 % menys de lectures. I la dada que cal endur-se: pthread_rwlock_t a glibc implementa per defecte la versió amb inanició, així que cal demanar PREFER_WRITER_NONRECURSIVE_NP a mà. Per a la proporció extrema de Meteora, la millor solució no és cap rwlock sinó la doble memòria intermèdia amb publicació atòmica del punter, on els lectors no prenen cap forrellat.
Els filòsofs comensals aïllen l'adquisició de diversos recursos, i la seva solució ingènua es bloqueja de manera impredictible —12 àpats en una execució, 94.203 en una altra—, que és la raó que aquestes fallades superin les proves. Les tres solucions pràctiques rendeixen gairebé idèntic (97-99 % de l'òptim teòric de 2.000 àpats/s): l'asimetria parell/senar, que és l'aplicació directa de l'ordre global de bloqueigs; un filòsof menys, que pel principi del colomar garanteix que algú avanci; i prendre totes dues forquilles atòmicament, que impedeix l'adquisició parcial. El barber dormilega és el grup de fils de meteo-api: treballadors que dormen a un 0 % de CPU esperant peticions, una cua acotada, i el rebuig explícit amb 503 quan s'omple —perquè rebutjar càrrega és una decisió de disseny, no una fallada, i una cua gran no augmenta la capacitat, només la latència—.
Queda un deute concret. Als filòsofs hem vist el cicle d'esperes i l'hem esquivat amb tres trucs, però no hem explicat per què funcionen ni què tenen en comú. Quines condicions exactes s'han de donar alhora perquè un interbloqueig sigui possible? Es pot detectar un que ja s'ha produït, o predir-lo abans de concedir un recurs? I com es diagnostica un servei real que s'ha quedat penjat a les tres de la matinada, quan no hi ha cap filòsof a la vista sinó un agregador i un meteo-api que no responen?
Ho tanquem a Interbloquejos: Prevenció, Detecció i Recuperació.
Fonaments de Sistemes Operatius
Mòdul 1: Introducció als Sistemes Operatius
- Conceptes Bàsics de Sistemes Operatius
- Història i Evolució dels Sistemes Operatius
- Tipus de Sistemes Operatius
- Funcions Principals d'un Sistema Operatiu
- Arquitectura del Nucli: Monolític, Microkernel i Híbrid
- Mode Usuari, Mode Nucli i Crides al Sistema
Mòdul 2: Gestió de Recursos
- Gestió de Processos
- Planificació de la CPU
- Gestió de Memòria
- Memòria Virtual i Paginació
- Gestió d'Emmagatzematge
- Gestió de Dispositius
- Controladors, Interrupcions i Operacions d'E/S
Mòdul 3: Concurrència
- Conceptes de Concurrència
- Fils i Processos
- Comunicació entre Processos (IPC)
- Sincronització i Exclusió Mútua
- Problemes Clàssics de Concurrència
- Interbloquejos: Prevenció, Detecció i Recuperació
Mòdul 4: Estructures de Fitxers
- Sistemes de Fitxers
- Estructures de Directoris
- Particions, Muntatge i Sistema de Fitxers Virtual
- Gestió de Fitxers
- Assignació d'Espai, Journaling i Integritat
- Seguretat i Permisos de Fitxers
Mòdul 5: Protecció i Seguretat del Sistema
- Principis de Protecció i Control d'Accés
- Usuaris, Autenticació i Escalada de Privilegis
- Amenaces Habituals i Enfortiment del Sistema
- Auditoria, Registres i Resposta a Incidents
Mòdul 6: Virtualització i Contenidors
- Virtualització: Hipervisors i Màquines Virtuals
- Contenidors: Namespaces i cgroups
- El Sistema Operatiu al Núvol
- Sistemes Operatius Mòbils i de Temps Real
