Concurrencia y subprocesamiento múltiple en JavaScript: nivel avanzado
\n\nTradicionalmente, JavaScript se considera un lenguaje de un solo subproceso, sin embargo, el ecosistema moderno de Node.js proporciona herramientas potentes para trabajar con concurrencia y verdadero subprocesamiento múltiple. En este artículo, analizaremos técnicas avanzadas: desde los detalles del Event Loop hasta el uso de Worker Threads, SharedArrayBuffer y Atomics. Aprenderá a distribuir la carga de manera eficiente, evitar condiciones de carrera y construir aplicaciones escalables.
\n\n1. Event Loop, microtareas y macrotareas: el fundamento de la concurrencia
\n\nComprender el Event Loop es la clave para gestionar la concurrencia sin bloqueos. En Node.js, el ciclo de eventos consta de varias fases: temporizadores, callbacks de I/O, idle, poll, check (setImmediate) y close. Es importante distinguir entre microtareas (Promise, process.nextTick) y macrotareas (setTimeout, setInterval, I/O).
\n\nLas microtareas se ejecutan inmediatamente después de que finaliza la fase actual, antes de la siguiente macrotarea. Esto puede provocar una "inanición" del Event Loop si las microtareas se generan infinitamente.
\n\n// Ejemplo: microtareas bloquean macrotareas\nsetTimeout(() => console.log('Macrotarea'), 0);\n\nPromise.resolve().then(() => {\n // Microtarea infinita\n process.nextTick(() => {\n console.log('Microtarea');\n // La llamada recursiva bloqueará el Event Loop\n process.nextTick(() => process.nextTick(() => {}));\n });\n});\n// Salida: 'Microtarea' (infinitamente), 'Macrotarea' nunca se ejecutará\n\nPara un control avanzado, use queueMicrotask() y setImmediate(). Recuerde: process.nextTick tiene prioridad sobre las microtareas de Promise.
2. Worker Threads: verdadero subprocesamiento múltiple en Node.js
\n\nEl módulo worker_threads permite ejecutar JavaScript en subprocesos paralelos. Cada Worker tiene su propia instancia de V8 y memoria aislada. El intercambio de datos se realiza a través de mensajes o SharedArrayBuffer.
Creación de un Worker simple:
\n\n// main.js\nconst { Worker } = require('worker_threads');\n\nfunction runWorker(file, data) {\n return new Promise((resolve, reject) => {\n const worker = new Worker(file, { workerData: data });\n worker.on('message', resolve);\n worker.on('error', reject);\n worker.on('exit', (code) => {\n if (code !== 0) reject(new Error(`Worker detenido con código ${code}`));\n });\n });\n}\n\n// Ejecución\nrunWorker('./worker.js', { iterations: 1e9 })\n .then(result => console.log('Resultado:', result))\n .catch(err => console.error(err));\n\n// worker.js\nconst { parentPort, workerData } = require('worker_threads');\n\nfunction heavyComputation(n) {\n let sum = 0;\n for (let i = 0; i < n; i++) { sum += Math.sqrt(i); }\n return sum;\n}\n\nparentPort.postMessage(heavyComputation(workerData.iterations));\n\nImportante: El Worker no tiene acceso al DOM, a los objetos globales del proceso (process.env) ni a los módulos cargados en el subproceso principal. Transfiera solo datos serializables.
\n\nPool de Workers
\nCrear un Worker para cada tarea es costoso. Use un pool:
\n\nclass WorkerPool {\n constructor(file, size = 4) {\n this.workers = [];\n this.queue = [];\n for (let i = 0; i < size; i++) {\n const worker = new Worker(file);\n worker.on('message', (result) => this._handleResult(worker, result));\n worker.on('error', (err) => this._handleError(worker, err));\n this.workers.push(worker);\n }\n }\n\n runTask(data) {\n return new Promise((resolve, reject) => {\n this.queue.push({ data, resolve, reject });\n this._processQueue();\n });\n }\n\n _processQueue() {\n if (this.queue.length === 0) return;\n const worker = this.workers.find(w => !w.busy);\n if (!worker) return;\n worker.busy = true;\n const task = this.queue.shift();