Concurrencia y multihilo en JavaScript: nivel avanzado
JavaScript ha sido históricamente un lenguaje de un solo hilo que funciona dentro del bucle de eventos (Event Loop). Sin embargo, las aplicaciones modernas en Node.js requieren un alto rendimiento al procesar tareas intensivas en CPU. En este artículo, analizaremos los mecanismos avanzados de concurrencia y multihilo: Worker Threads, SharedArrayBuffer, Atomics y clustering. Aprenderá cómo utilizar eficazmente los procesadores multinúcleo y evitar el bloqueo del hilo principal.
1. Worker Threads: verdadero multihilo en Node.js
El módulo worker_threads apareció en Node.js 10.5.0 y permite crear hilos separados con su propio motor V8. A diferencia del clustering (procesos), Worker Threads comparten memoria y pueden intercambiar datos a través de SharedArrayBuffer.
Creación y ejecución de un worker
// main.jsconst { Worker } = require('worker_threads');
function runWorker(data) { return new Promise((resolve, reject) => { const worker = new Worker('./worker.js', { workerData: data }); worker.on('message', resolve); worker.on('error', reject); worker.on('exit', (code) => { if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`)); }); });}
(async () => { const result = await runWorker({ numbers: [1, 2, 3, 4, 5] }); console.log('Resultado:', result); // { sum: 15 }})();// worker.jsconst { parentPort, workerData } = require('worker_threads');
const sum = workerData.numbers.reduce((acc, val) => acc + val, 0);parentPort.postMessage({ sum });Importante: Los workers no tienen acceso al DOM ni a módulos que requieran el hilo principal. Úselos para cálculos pesados, procesamiento de imágenes o cifrado.
2. SharedArrayBuffer y Atomics: memoria compartida
SharedArrayBuffer permite que varios workers lean y escriban datos en una misma área de memoria. Para sincronizar el acceso se utiliza el objeto Atomics, que garantiza operaciones atómicas.
Ejemplo: procesamiento paralelo de un array
// main.jsconst { Worker } = require('worker_threads');const { SharedArrayBuffer } = require('worker_threads');
const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 10);const array = new Int32Array(buffer);
// Llenamos el arrayfor (let i = 0; i < 10; i++) array[i] = i;
const worker1 = new Worker('./worker_atomics.js', { workerData: { start: 0, end: 5 } });const worker2 = new Worker('./worker_atomics.js', { workerData: { start: 5, end: 10 } });
worker1.on('message', () => console.log('Worker 1 terminó'));worker2.on('message', () => console.log('Worker 2 terminó'));// worker_atomics.jsconst { parentPort, workerData } = require('worker_threads');const { Atomics } = require('worker_threads');
const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 10);const array = new Int32Array(buffer);
// Incremento atómico de cada elementofor (let i = workerData.start; i < workerData.end; i++) { Atomics.add(array, i, 10);}
parentPort.postMessage('done');Nota: Use Atomics.wait() y Atomics.notify() para la sincronización de hilos (análogo a los mutex). Esto es fundamental para prevenir condiciones de carrera.
3. Clustering: escalado mediante procesos
El módulo cluster crea varios procesos de Node.js (workers), cada uno de los cuales maneja solicitudes. Esta es la forma estándar de escalar servidores HTTP en sistemas multinúcleo.
Ejemplo: servidor HTTP clusterizado
const cluster = require('cluster');const http = require('http');const numCPUs = require('os').cpus().length;
if (cluster.isMaster) { console.