Concurrencia y subprocesamiento múltiple en TypeScript: de la A a la Z
En el mundo moderno del desarrollo web, el rendimiento de las aplicaciones pasa a primer plano. TypeScript, siendo una superposición fuertemente tipada sobre JavaScript, proporciona a los desarrolladores herramientas potentes para trabajar con asincronía y subprocesamiento múltiple. En este artículo, analizaremos los conceptos fundamentales de la concurrencia, aprenderemos cómo funciona el Event Loop y aprenderemos a usar Worker Threads para un verdadero subprocesamiento múltiple en Node.js.
1. Asincronía y Event Loop: la base de la concurrencia en TypeScript
Antes de hablar sobre subprocesamiento múltiple, es importante entender cómo TypeScript (y JavaScript) maneja las tareas concurrentes. En el centro se encuentra el Event Loop, un mecanismo que permite ejecutar operaciones de entrada/salida no bloqueantes sin crear nuevos hilos.
Consideremos un ejemplo simple de código asíncrono:
async function fetchData(url: string): Promise<string> { const response = await fetch(url); return response.text();}
async function main(): Promise<void> { console.log('Inicio de la carga'); const data = await fetchData('https://api.example.com/data'); console.log('Datos recibidos:', data);}
main();console.log('El código continúa ejecutándose');En este ejemplo, await no bloquea el hilo principal. Mientras se espera la respuesta del servidor, el Event Loop continúa procesando otras tareas, como la ejecución de console.log. Esto es concurrencia: la capacidad de manejar múltiples tareas en un período de tiempo, alternando entre ellas.
2. Promesas y async/await: gestión de operaciones asíncronas
TypeScript proporciona construcciones sintácticas convenientes para trabajar con asincronía. Las Promesas (Promises) son objetos que representan el resultado futuro de una operación asíncrona. La combinación de async/await hace que el código sea lineal y legible.
Consideremos un escenario más complejo: la ejecución paralela de múltiples solicitudes:
async function loadMultipleUsers(userIds: number[]): Promise<User[]> { const promises = userIds.map(id => fetch(`/users/${id}`).then(res => res.json())); return Promise.all(promises);}
// Usoconst users = await loadMultipleUsers([1, 2, 3, 4, 5]);console.log(users); // Todos los usuarios se cargan en paraleloEl método Promise.all ejecuta todas las promesas simultáneamente, lo que acelera significativamente la ejecución en comparación con la carga secuencial. Este es un ejemplo perfecto de concurrencia: las tareas se ejecutan "simultáneamente" dentro de un solo hilo.
3. Verdadero subprocesamiento múltiple: Worker Threads en Node.js
Aunque la asincronía maneja bien las operaciones de entrada/salida, no ayuda con tareas intensivas en CPU (procesamiento de imágenes, cálculos complejos, cifrado). Para estos casos, Node.js tiene Worker Threads: hilos reales del sistema operativo.
Consideremos un ejemplo de cálculo factorial en un hilo separado:
// worker.tsimport { parentPort } from 'worker_threads';
function factorial(n: number): number { if (n === 0) return 1; return n * factorial(n - 1);}
parentPort?.on('message', (n: number) => { const result = factorial(n); parentPort?.postMessage(result);});
// main.tsimport { Worker } from 'worker_threads';
function runWorker(n: number): Promise<number> { return new Promise((resolve, reject) => { const worker = new Worker('./worker.ts'); worker.postMessage(n); worker.on('message', resolve); worker.on('error', reject); });}
async function main() { const result = await runWorker(20); console.log('Factorial de 20:', result);}
main();Cada Worker funciona en su propio hilo, tiene su propio Event Loop y se