La lliçó anterior va repartir Escena Viva entre set treballadors i va multiplicar per gairebé cinc la capacitat del servidor. Però va deixar un problema intacte i ho vam dir sense embuts: cluster no desbloqueja el bucle d'esdeveniments. Quan un treballador es posa a compondre el PDF amb el codi QR de les 500 entrades de l'estrena del Festival de Jazz de Primavera (evt-003, Auditorio Ribera), aquell procés deixa d'atendre tothom durant el temps que duri el càlcul.
En aquesta lliçó mesurem aquest blocatge amb l'instrumental del mòdul 2, entenem per què els fils són l'eina correcta per al treball de CPU (i cluster no ho és), i construïm a Escena Viva un PoolDeFils propi, perquè crear un fil per petició és pitjor que no fer servir fils.
Contingut
- El blocatge, mesurat amb
monitorEventLoopDelay - Cluster davant de worker threads: la regla de decisió
node:worker_threads: el mecanisme bàsic- Clonatge estructurat: què viatja entre fils i què costa
- Transferència davant de còpia:
ArrayBuffer,SharedArrayBufferiAtomics - El fil d'Escena Viva:
src/treballadors/generar-entrades.js PoolDeFils: per què un fil per petició és un error- Errors dins del fil i com es propaguen
- Mesura abans i després
- Quan NO fer servir fils
- El blocatge, mesurat amb
monitorEventLoopDelay
monitorEventLoopDelayAl mòdul 3 vam deixar src/utils/qr-entrada.js amb { codificarQr, descodificarQr }: signa el contingut amb HMAC i el codifica en base64url. En generar les entrades d'una sessió sencera cal fer-ho una vegada per entrada, més la composició del PDF. És treball síncron i de CPU: no hi ha E/S per delegar, només càlcul.
El mesurarem abans d'opinar. Aquest script simula la generació de les 500 entrades de l'estrena mentre un temporitzador intenta bategar cada 20 ms:
'use strict';
const { monitorEventLoopDelay } = require('node:perf_hooks');
const { generarLotDEntrades } = require('../src/serveis/entrades-pdf.js');
const histograma = monitorEventLoopDelay({ resolution: 10 });
histograma.enable();
// Batec regular: si el bucle esta lliure, s'executa cada ~20 ms.
let batecs = 0;
const batec = setInterval(() => { batecs += 1; }, 20);
(async function mesurar() {
const inici = process.hrtime.bigint();
await generarLotDEntrades({ sessioId: 'ses-003-1', esdevenimentId: 'evt-003', quantitat: 500 });
const msTotals = Number(process.hrtime.bigint() - inici) / 1e6;
clearInterval(batec);
histograma.disable();
console.log(`Generacio: ${msTotals.toFixed(0)} ms`);
console.log(`Batecs: ${batecs} (esperats ~${Math.round(msTotals / 20)})`);
console.log(`Retard maxim: ${(histograma.max / 1e6).toFixed(2)} ms`);
})();Resultat a la màquina de referència: Generacio: 4180 ms, Batecs: 3 (esperats ~209), Retard maxim: 4176.44 ms.
Tres batecs on n'hi hauria d'haver 209. Durant 4,18 segons el procés no va executar absolutament res més: ni va respondre peticions, ni va atendre la connexió a PostgreSQL, ni va processar el keep-alive. Amb SCHED_RR, el primari li va continuar enviant connexions —no sap que està ocupat— i aquelles connexions es van quedar a la cua del sistema. Quan el treballador va tornar, tenia un embús esperant-lo i el percentil 99 de tota l'aplicació es va disparar. Amb 7 treballadors el mal és 1/7 del trànsit; però si tres organitzadors generen entrades alhora (una cosa normal la nit de l'estrena), tres de set treballadors estan congelats.
- Cluster davant de worker threads: la regla de decisió
| Aspecte | cluster (processos) |
worker_threads (fils) |
|---|---|---|
| Unitat | Procés del sistema operatiu | Fil dins del mateix procés |
| Instància de V8 | Una per procés | Una per fil (aïllades) |
| Bucle d'esdeveniments | Un per procés | Un per fil |
| Munt | Independent, sense compartir | Independent, però es pot compartir memòria amb SharedArrayBuffer |
| Cost d'arrencada | ~40-80 ms (procés Node complet) | ~10-25 ms |
| Cost de memòria | ~40-80 MB per procés | ~5-15 MB per fil |
| Comunicació | IPC serialitzat (JSON o avançat) | postMessage amb clonatge estructurat, o memòria compartida |
| Comparteixen port | Sí (el primari reparteix) | No (però poden compartir un socket amb net, avançat) |
| Una fallada mata... | Només aquell procés | Només aquell fil, però un SharedArrayBuffer corrupte afecta tothom |
| Serveix per a | Concurrència d'E/S: més peticions alhora | Treball de CPU: no bloquejar el fil principal |
La regla de decisió, que convé memoritzar: cluster per escalar l'E/S, fils per treure la CPU del camí. No són alternatives: a Escena Viva farem servir totes dues. Set treballadors de cluster, i dins de cadascun un pool petit de fils per a la feina pesada. I compte amb l'aritmètica: 7 treballadors × 4 fils = 28 fils de CPU en una màquina de 8 nuclis és sobrevenda. Hi tornarem a l'apartat 7.
node:worker_threads: el mecanisme bàsic
node:worker_threads: el mecanisme bàsicEl mòdul exposa l'essencial en molt poques peces:
| Element | On viu | Per a què |
|---|---|---|
new Worker(ruta, opcions) |
Fil principal | Crea el fil i executa el fitxer indicat |
workerData |
Fil treballador | Dades inicials clonades a l'arrencada |
parentPort |
Fil treballador | Port de comunicació amb qui l'ha creat |
isMainThread / threadId |
Tots dos | En quin fil som i amb quin identificador |
MessageChannel / MessagePort |
Tots dos | Canals addicionals entre fils qualssevol |
Un exemple mínim, amb els esdeveniments que importen:
'use strict';
const path = require('node:path');
const { Worker } = require('node:worker_threads');
function executarEnFil(ruta, dades) {
return new Promise((resoldre, rebutjar) => {
const fil = new Worker(path.join(__dirname, ruta), { workerData: dades });
fil.on('message', (resultat) => resoldre(resultat));
// 'error': excepcio no capturada dins del fil. 'online': el fil
// ja executa JavaScript (util per mesurar el cost d'arrencada).
fil.on('error', (error) => rebutjar(error));
fil.on('exit', (codi) => {
if (codi !== 0) rebutjar(new Error(`El fil ha acabat amb codi ${codi}`));
});
});
}I el fitxer del fil, on workerData arriba ja clonat (és una còpia, no una referència):
'use strict';
const { parentPort, workerData, threadId } = require('node:worker_threads');
const suma = workerData.nombres.reduce((acumulat, valor) => acumulat + valor, 0);
parentPort.postMessage({ fil: threadId, suma });Dos matisos importants. Primer, postMessage no acaba el fil; el fil viu fins que es queda sense feina pendent o algú crida terminate(), i pot continuar rebent tasques per parentPort.on('message', ...). Això és precisament el que permet reutilitzar-lo en un pool. Segon, cada fil carrega els seus propis mòduls: si generar-entrades.js fa require de Sequelize, aquell require s'executa una vegada per fil, amb el seu cost d'arrencada i de memòria.
- Clonatge estructurat: què viatja entre fils i què costa
Els missatges entre fils no comparteixen memòria per defecte: es copien amb l'algorisme de clonatge estructurat (el mateix de postMessage al navegador i de structuredClone). És més capaç que JSON.stringify, però té límits estrictes.
| Tipus | Viatja? | Nota |
|---|---|---|
Primitius (null, undefined, BigInt inclosos) |
Sí | |
| Objectes i arrays plans | Sí | Incloses les referències circulars |
Date, RegExp |
Sí | Es conserven com a tals, no com a cadenes |
Map, Set |
Sí | Avantatge clar sobre JSON |
Buffer, TypedArray, ArrayBuffer |
Sí | Copiat, llevat que es transfereixi |
Error |
Sí | Es conserven message, name i stack |
| Funcions | No | DataCloneError |
| Classes (la identitat) | No | Arriba l'objecte pla, sense prototip ni mètodes |
Getters, símbols, WeakMap |
No | Es perden silenciosament o fallen |
| Connexions, sockets, handles de fs | No | No tenen sentit fora del seu fil |
Conseqüència directa per a Escena Viva: no pots enviar una instància d'Entrada ni de Comanda i esperar que conservi els seus mètodes. El nostre domini (src/domini/) fa servir classes; en creuar la frontera del fil cal serialitzar a dades planes i reconstruir a l'altre costat, i per això ho dissenyarem així: el fil rep dades primitives i retorna dades primitives. El cost de copiar tampoc no és zero: clonar 500 objectes d'entrada és de l'ordre d'un mil·lisegon, però clonar un PDF de 8 MB com a Buffer és de l'ordre de desenes. Si el resultat és gran, transfereix-lo en comptes de copiar-lo.
- Transferència davant de còpia:
ArrayBuffer, SharedArrayBuffer i Atomics
ArrayBuffer, SharedArrayBuffer i AtomicspostMessage accepta un segon argument amb la llista d'objectes transferibles: parentPort.postMessage({ dades: arrayBuffer }, [arrayBuffer]). Un ArrayBuffer transferit no es copia, canvia d'amo, i el fil emissor es queda amb una memòria intermèdia desconnectada (byteLength === 0). És la manera correcta de retornar un PDF de diversos megabytes, i la veurem aplicada a l'apartat 6.
SharedArrayBuffer va un pas més enllà: la mateixa memòria és visible des de diversos fils alhora, sense còpia ni transferència. És l'eina per a comptadors compartits, com el nombre d'entrades generades a l'estrena:
'use strict';
// Al fil principal: 2 enters de 32 bits compartits, que s'envien
// a cada fil a workerData SENSE copiar-se.
// comptadors[0] = entrades generades, comptadors[1] = errors.
const comptadors = new Int32Array(new SharedArrayBuffer(8));
// Atomics.add llegeix, suma i escriu sense que un altre fil pugui
// collar-s'hi enmig. Amb comptadors[0] += 1 perdriem increments.
const registrarEntradaGenerada = (c) => Atomics.add(c, 0, 1);
// Atomics.load garanteix veure el valor mes recent publicat per
// qualsevol fil, sense reordenacions del compilador o la CPU.
const llegirTotal = (c) => Atomics.load(c, 0);
module.exports = { comptadors, registrarEntradaGenerada, llegirTotal };Advertiment seriós. Fins ara, en JavaScript d'un sol fil, les condicions de carrera de memòria no existien: entre dues línies de la teva funció ningú no podia modificar les teves variables. SharedArrayBuffer obre aquesta porta. comptadors[0] += 1 són tres operacions (llegir, sumar, escriure) i dos fils poden entrellaçar-les i perdre increments. Per això existeix Atomics (add, load, store, compareExchange, wait, notify). Fes-lo servir sempre per tocar memòria compartida, i fes servir memòria compartida només quan de debò calgui: per a dades estructurades, el clonatge és més lent però infinitament més segur.
- El fil d'Escena Viva:
src/treballadors/generar-entrades.js
src/treballadors/generar-entrades.jsEl fil rep les dades planes d'un lot d'entrades, genera per a cadascuna el seu QR signat reutilitzant codificarQr del mòdul 3, compon el PDF i retorna el resultat transferint la memòria intermèdia.
'use strict';
const { parentPort } = require('node:worker_threads');
const { codificarQr } = require('../utils/qr-entrada.js');
const { compondrePdfDEntrades } = require('../serveis/pdf.js');
// Genera el QR i el PDF d'un lot d'entrades. Feina purament de CPU.
function generarLot({ comandaId, esdevenimentTitol, salaNom, dataSessio, entrades }) {
const entradesAmbQr = entrades.map((entrada) => ({
codi: entrada.codi, // format EV-2026-004871
butaca: entrada.butaca,
preuCentims: entrada.preuCentims,
// codificarQr signa amb HMAC i retorna base64url (modul 3).
qr: codificarQr({ codi: entrada.codi, sessioId: entrada.sessioId }),
}));
const bufferPdf = compondrePdfDEntrades({
comandaId, esdevenimentTitol, salaNom, dataSessio, entrades: entradesAmbQr,
});
return { bufferPdf, generades: entradesAmbQr.length };
}
// El fil es mante viu escoltant tasques: aixi el reutilitza el pool.
parentPort.on('message', ({ idTasca, peticio }) => {
try {
const { bufferPdf, generades } = generarLot(peticio);
const arrayBuffer = bufferPdf.buffer.slice(
bufferPdf.byteOffset, bufferPdf.byteOffset + bufferPdf.byteLength
);
parentPort.postMessage(
{ idTasca, ok: true, resultat: { pdf: arrayBuffer, generades } },
[arrayBuffer] // transferencia sense copia
);
} catch (error) {
// Mai no deixem que l'excepcio mati el fil: la retornem com a dada.
parentPort.postMessage({
idTasca, ok: false,
error: { missatge: error.message, nom: error.name, pila: error.stack },
});
}
});Fixa't en la decisió de disseny: el fil no llança excepcions cap amunt, les converteix en respostes. Un throw sense capturar dispararia l'esdeveniment error i mataria el fil, obligant el pool a crear-ne un altre. Capturar a dins i respondre amb ok: false manté el fil reutilitzable.
PoolDeFils: per què un fil per petició és un error
PoolDeFils: per què un fil per petició és un errorHem mesurat el cost d'arrencada d'un fil que fa require del nostre codi: entre 18 i 40 ms. Si la tasca dura 4 000 ms, aquest cost és soroll. Però per a lots petits —una entrada solta de Lucía per a ses-001-1— la tasca dura 12 ms i l'arrencada costa el triple que la feina. Encara pitjor: 50 peticions simultànies crearien 50 fils, és a dir 50 instàncies de V8 competint per 8 nuclis, amb centenars de megabytes de memòria i un col·lapse per canvi de context.
La solució és un pool: un nombre fix i petit de fils creats a l'arrencada, una cua de tasques, i reutilització.
'use strict';
const path = require('node:path');
const os = require('node:os');
const crypto = require('node:crypto');
const { Worker } = require('node:worker_threads');
const RUTA_FIL = path.join(__dirname, 'generar-entrades.js');
class PoolDeFils {
// mida petita per defecte: la feina de CPU no escala mes enlla dels
// nuclis, i convivim amb els treballadors de cluster (10-01).
constructor({ mida, rutaFil = RUTA_FIL, tempsLimitMs = 30_000 } = {}) {
this.mida = mida || Math.max(1, Math.floor(os.availableParallelism() / 2));
this.rutaFil = rutaFil;
this.tempsLimitMs = tempsLimitMs;
this.fils = []; // tots els fils vius
this.lliures = []; // fils sense tasca assignada
this.cua = []; // tasques esperant fil
this.pendents = new Map(); // idTasca -> { resoldre, rebutjar, temporitzador }
this.tancat = false;
for (let i = 0; i < this.mida; i += 1) this.crearFil();
}
crearFil() {
const fil = new Worker(this.rutaFil);
fil.tascaActual = null;
fil.on('message', (missatge) => {
const pendent = this.pendents.get(missatge.idTasca);
if (pendent) {
clearTimeout(pendent.temporitzador);
this.pendents.delete(missatge.idTasca);
if (missatge.ok) pendent.resoldre(missatge.resultat);
else pendent.rebutjar(Object.assign(new Error(missatge.error.missatge), {
name: missatge.error.nom, stack: missatge.error.pila,
}));
}
this.retornarFil(fil);
});
// El fil ha mort per una excepcio no capturada: rebutgem la seva tasca
// en curs i el substituim per mantenir la mida del pool.
fil.on('error', (error) => {
this.rebutjarTascaDe(fil, error);
this.retirarFil(fil);
if (!this.tancat) this.crearFil();
});
fil.on('exit', (codi) => {
if (codi !== 0 && !this.tancat) { this.retirarFil(fil); this.crearFil(); }
});
this.fils.push(fil);
this.lliures.push(fil);
}
retirarFil(fil) {
this.fils = this.fils.filter((altre) => altre !== fil);
this.lliures = this.lliures.filter((altre) => altre !== fil);
}
rebutjarTascaDe(fil, error) {
const pendent = fil.tascaActual && this.pendents.get(fil.tascaActual);
if (pendent) {
clearTimeout(pendent.temporitzador);
this.pendents.delete(fil.tascaActual);
pendent.rebutjar(error);
}
fil.tascaActual = null;
}
// En quedar lliure, el fil pren la tasca seguent de la cua si n'hi ha.
retornarFil(fil) {
fil.tascaActual = null;
const seguent = this.cua.shift();
if (seguent) this.assignar(fil, seguent);
else this.lliures.push(fil);
}
assignar(fil, tasca) {
fil.tascaActual = tasca.idTasca;
fil.postMessage({ idTasca: tasca.idTasca, peticio: tasca.peticio });
}
// API publica: retorna una promesa amb el resultat del fil.
executar(peticio) {
if (this.tancat) return Promise.reject(new Error('El pool esta tancat'));
const idTasca = crypto.randomUUID();
return new Promise((resoldre, rebutjar) => {
const temporitzador = setTimeout(() => {
this.pendents.delete(idTasca);
rebutjar(new Error(`La tasca ${idTasca} ha superat ${this.tempsLimitMs} ms`));
}, this.tempsLimitMs);
this.pendents.set(idTasca, { resoldre, rebutjar, temporitzador });
const fil = this.lliures.pop();
if (fil) this.assignar(fil, { idTasca, peticio });
else this.cua.push({ idTasca, peticio }); // tots ocupats: espera torn
});
}
// estat() exposa { mida, lliures, enCua } per al punt d'entrada de salut.
// tancar() marca el pool com a tancat i fa terminate() de cada fil,
// i s'enganxa a l'aturada ordenada del M6.
}
module.exports = { PoolDeFils };Sobre la mida del pool: per a feina purament de CPU, més fils que nuclis no aporta res, només canvi de context. I com que cada treballador de cluster té el seu propi pool, el càlcul real és treballadorsCluster × midaDelPool ≤ nuclis. Amb 7 treballadors en 8 nuclis, el pool hauria de ser d'1 o 2 fils. Aquesta tensió —cluster i fils competint pels mateixos nuclis— és la raó per la qual moltes arquitectures acaben traient la feina pesada del servidor web del tot, cap a un procés a part amb una cua: la lliçó 10-03. En producció, a més, la biblioteca piscina fa tot això i més (cancel·lació amb AbortSignal, reciclatge de fils després de N tasques, límits de cua, mètriques); hem escrit el nostre per entendre què fa per dins.
El servei d'aplicació queda així de simple:
'use strict';
// Un unic pool per proces, creat a l'arrencada. Mai un per peticio.
function crearServeiDEntrades({ poolDeFils }) {
return {
async generarPdfDeComanda(dadesComanda) {
const { pdf, generades } = await poolDeFils.executar(dadesComanda);
// 'pdf' arriba com a ArrayBuffer transferit; l'emboliquem sense copiar.
return { buffer: Buffer.from(pdf), generades };
},
};
}
module.exports = { crearServeiDEntrades };Encaixa sense fricció amb la refacció del mòdul 9: els controladors són factories amb dependències injectades, així que el pool s'injecta com una dependència més i a les proves se substitueix per un doble de Sinon.
- Errors dins del fil i com es propaguen
| Situació al fil | Què passa | Com ho gestiona el pool |
|---|---|---|
throw capturat pel nostre try/catch |
Es respon { ok: false, error } |
Rebutja la promesa; el fil continua viu |
throw no capturat |
Esdeveniment error al pare; el fil mor |
Rebutja la tasca en curs, retira i recrea el fil |
| Rebuig de promesa no gestionat | Esdeveniment error (política per defecte) |
Igual que l'anterior |
process.exit() dins del fil |
Esdeveniment exit amb codi |
Recrea el fil; la tasca queda òrfena si no la rebutgem |
| Bucle infinit | Res: el fil no respon mai | Salta el tempsLimitMs i es rebutja; convé terminate() |
| Memòria exhaurida | Mor el fil (o el procés sencer) | Limita la mida dels lots |
Un matís sobre el temps límit: el nostre executar rebutja la promesa, però el fil continua calculant. Per a un bucle infinit real cal cridar terminate() i recrear el fil (exercici 2).
- Mesura abans i després
Escenari: estrena del Festival de Jazz, amb una càrrega base de 500 peticions per segon al catàleg i, cada 2 segons, un organitzador que sol·licita el PDF d'un lot de 500 entrades.
| Mètrica | Sense fils (síncron) | Amb PoolDeFils (2 fils) |
|---|---|---|
| Retard màxim del bucle d'esdeveniments | 4 176 ms | 11 ms |
| Latència p50 del catàleg | 1 240 ms | 18 ms |
| Latència p99 del catàleg | 6 810 ms | 74 ms |
| Peticions amb 503 / temps exhaurit | 217 | 0 |
| Durada de la generació del lot | 4 180 ms | 4 390 ms |
Llegeix l'última fila amb atenció: la generació triga una mica més (4 390 davant de 4 180 ms), perquè cal clonar la petició d'entrada, transferir el resultat i coordinar el pool. Els fils no acceleren la tasca pesada; el que fan és treure-la del camí de tots els altres. El p99 del catàleg cau de 6,8 segons a 74 mil·lisegons, i aquesta és l'única xifra que importa a qui està intentant comprar una entrada mentre es genera el lot.
- Quan NO fer servir fils
Els fils tenen un cost real (memòria, arrencada, complexitat, serialització). No els facis servir si:
- La tasca és d'E/S. Llegir un fitxer, consultar PostgreSQL, cridar l'API de divises: això ja és asíncron i no bloqueja res. Ficar una consulta SQL en un fil no l'accelera; només afegeix una còpia de dades i un salt entre fils. Aquest és el malentès més freqüent.
- La tasca és trivial. Si la feina dura 2 ms, el clonatge i el traspàs costen més que fer-la al mateix lloc. Llindar orientatiu: per sota de ~10-20 ms de CPU, no compensa.
- La tasca es pot trossejar. Al mòdul 2 vam veure com partir un bucle llarg amb
setImmediateper retornar el control al bucle entre trossos. Si la feina es deixa tallar netament i no és enorme, trossejar és més simple que un pool.
'use strict';
// Alternativa del modul 2: trossejar en lloc de fer servir fils. Processa
// el lot en blocs de 25 i cedeix el control entre blocs, de manera que el
// servidor respon peticions entre bloc i bloc.
function processarPerBlocs(elements, processar, midaBloc = 25) {
return new Promise((resoldre) => {
const resultats = [];
let index = 0;
(function seguentBloc() {
const fi = Math.min(index + midaBloc, elements.length);
for (; index < fi; index += 1) resultats.push(processar(elements[index]));
if (index < elements.length) setImmediate(seguentBloc);
else resoldre(resultats);
})();
});
}Trossejar redueix el retard màxim del bucle a la durada d'un bloc (uns 200 ms en el nostre cas), cosa que ja és acceptable per a moltes aplicacions, i no costa ni un fil ni un megabyte. La contrapartida és que la tasca total triga més i continua consumint l'únic fil principal.
- La tasca és realment llarga. Si generar i enviar per correu les 500 entrades de l'estrena triga minuts, ni el fil ni el trossejat són la resposta: la petició HTTP no hauria d'esperar gens. Cal treure la feina del procés i encuar-la, respondre
202 Acceptedamb un identificador, i que el client consulti l'estat. Aquest és el tema de la lliçó següent.
Errors Comuns i Consells
- Crear un
Workerper petició. L'error més car. Pool sempre, creat en arrencar el procés. - Enviar instàncies de classes del domini. Arriben com a objectes plans sense mètodes: serialitza a dades i reconstrueix a l'altre costat.
- Ficar E/S en un fil. No millora res: l'E/S ja era asíncrona.
- Fer servir
SharedArrayBuffersenseAtomics, o oblidar que unArrayBuffertransferit queda buit a l'emissor. - No posar temps límit a les tasques. Un fil penjat reté la seva plaça del pool per sempre i acaba aturant tota la generació.
- Consell: exposa
pool.estat()al teu punt d'entrada de salut. Una cua que creix sense parar et diu que el pool està infradimensionat o que cal encuar fora del procés. - Consell: mesura sempre amb
monitorEventLoopDelayabans i després. Si el retard no baixa, els fils no eren el problema.
Exercicis
Exercici 1: demostrar el blocatge i mesurar-lo
Escriu un script que arrenqui un servidor HTTP mínim amb una ruta /bloquejar que calculi 500 hashos SHA-256 amb 100 000 iteracions cadascun de manera síncrona, i una ruta /ping que respongui { ok: true }. Amb autocannon contra /ping, mesura el p99 en repòs i mentre s'executa /bloquejar. Registra el retard del bucle amb monitorEventLoopDelay.
Exercici 2: afegir terminate al temps límit
Modifica PoolDeFils perquè, en exhaurir-se tempsLimitMs, a més de rebutjar la promesa, cridi terminate() sobre el fil blocat i el substitueixi. Comprova el comportament amb un fil que executi while (true) {}.
Exercici 3: comptador compartit amb Atomics
Amb 4 fils incrementant el mateix Int32Array sobre SharedArrayBuffer 100 000 vegades cadascun, compara el resultat fent servir comptadors[0] += 1 davant d'Atomics.add(comptadors, 0, 1). Explica la diferència.
Solucions
Exercici 1. En repòs, /ping dona un p99 de 2-4 ms. Durant /bloquejar, totes les peticions a /ping s'acumulen i el p99 puja a diversos segons: exactament la durada del càlcul síncron. L'histograma mostra un max gairebé idèntic a aquella durada, perquè el bucle no va fer ni una volta. La lectura correcta és que /ping no té cap problema: el problema és un veí sorollós al mateix fil. És l'argument sencer d'aquesta lliçó en 30 línies de codi.
Exercici 2. Dins del setTimeout d'executar, es localitza el fil amb this.fils.find((f) => f.tascaActual === idTasca), es retira del pool, es crida await filPenjat.terminate() —que mata el fil encara que estigui en bucle— i se'n crea un de nou abans de rebutjar la promesa. Amb while (true) {} al fil, sense terminate() la plaça del pool es perd per sempre i després de N tasques penjades el pool queda inutilitzable. Amb terminate(), el pool es recupera sol. Nota: terminate() mata el fil de manera immediata i no hi ha aturada ordenada possible a dins.
Exercici 3. Amb comptadors[0] += 1 el total esperat és 400 000 però l'observat ronda les 120 000-250 000, i varia a cada execució. El motiu és que += són tres passos (llegir, sumar, escriure) i dos fils poden llegir el mateix valor i escriure el mateix resultat, perdent un increment. Amb Atomics.add el total és exactament 400 000, sempre: l'operació és indivisible a nivell de CPU. La moralitat és doble: la memòria compartida introdueix una classe d'errors que en JavaScript no existia, i aquests errors són no deterministes, de manera que les teves proves del mòdul 9 poden passar mil vegades i fallar en producció la nit de l'estrena.
Conclusió
Hem convertit un blocatge de 4,18 segons en un retard d'11 mil·lisegons. Hem mesurat el problema amb monitorEventLoopDelay, hem distingit amb claredat quan toca cluster (concurrència d'E/S) i quan fils (treball de CPU), hem entès què viatja entre fils amb el clonatge estructurat i què no, hem après a transferir en comptes de copiar i a compartir memòria amb Atomics sense obrir la porta a condicions de carrera, i hem construït src/treballadors/generar-entrades.js i src/treballadors/pool.js amb cua, reutilització, temps límit i recuperació davant d'errors. També hem après els límits: els fils no acceleren la tasca, només l'aparten; no serveixen per a E/S; no compensen per a feina trivial; i competeixen pels mateixos nuclis que els treballadors de cluster, cosa que obliga a dimensionar amb cap.
I queda una peça fora de lloc. Generar el PDF ja no bloqueja, però l'organitzador de l'Auditorio Ribera continua esperant quatre segons i mig amb la petició HTTP oberta que acabi. Si a més cal enviar per correu aquelles 500 entrades, l'espera es torna absurda. La resposta no és un fil més ràpid: és no esperar. A la lliçó següent, Memòria Cau i Cues de Treball amb Redis, traurem la feina pesada fora del procés, respondrem 202 Accepted amb un identificador de treball, guardarem a la memòria cau el catàleg que avui es recalcula a cada petició, i de passada saldarem el deute que va deixar la lliçó 10-01: sessions i límits de peticions compartits entre tots els treballadors.
Curs de Node.js: De Principiant a Avançat
Mòdul 1: Introducció a Node.js
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
