Un solo hilo para gobernarlos a todos: mitos y verdades sobre la concurrencia en JavaScript
\n\nÉrase una vez, en la era de los sitios web estáticos y los botones "Enviar" que bloqueaban toda la interfaz, JavaScript vivía feliz en un solo hilo. Le bastaba: llegaba una solicitud, se ejecutaba; se iba una solicitud, la interfaz se congelaba. Pero los tiempos cambiaron. Hoy esperamos de las aplicaciones web una reacción instantánea: el desplazamiento no debe ralentizarse, las animaciones deben ser fluidas y la carga de datos, imperceptible. Y aquí es donde entra en juego la concurrencia.
\n\nMuchos desarrolladores confunden concurrencia (concurrency) con paralelismo (parallelism). En JavaScript, especialmente en el navegador, a menudo tratamos con la primera. Es como un malabarista: no sostiene todas las pelotas en el aire al mismo tiempo (eso es paralelismo), pero cambia entre ellas con tanta destreza que crea la ilusión de simultaneidad. En este artículo, analizaremos tres herramientas clave para organizar la concurrencia en JS: async/await, gorutinas (a través de generadores y Web Workers) y los hilos clásicos. ¡Allá vamos!
Async/Await: código asíncrono elegante
\n\nasync/await es azúcar sintáctico sobre las Promises (Promesas), que apareció en ES2017. No hace que el código sea multihilo, pero lo hace asíncrono. Cuando escribes await, JavaScript se "duerme" en esa función, liberando el hilo para otras tareas. Tan pronto como la promesa se resuelve, la función se despierta y continúa su ejecución. Es la herramienta ideal para operaciones de I/O: solicitudes a API, lectura de archivos, temporizadores.
Ejemplo simple: cargar datos desde el servidor
\n\n// Función que devuelve una Promise\nfunction fetchUser(id) {\n return new Promise((resolve) => {\n setTimeout(() => resolve({ id, name: 'Alejandro' }), 1000);\n });\n}\n\n// Función asíncrona\nasync function loadUserProfile(userId) {\n console.log('Comenzando la carga...');\n const user = await fetchUser(userId);\n console.log('Usuario cargado:', user.name);\n return user;\n}\n\nloadUserProfile(42);\nconsole.log('¡Este código se ejecutará de inmediato, sin esperar la carga!');\n\n\nObserva: console.log('¡Este código se ejecutará de inmediato...') se ejecutará antes que console.log('Usuario cargado'). El hilo no se bloquea: esto es concurrencia en acción.
Manejo de errores
\n\nasync function fetchWithRetry(url, retries = 3) {\n for (let i = 0; i < retries; i++) {\n try {\n const response = await fetch(url);\n if (!response.ok) throw new Error('Error HTTP');\n return await response.json();\n } catch (error) {\n if (i === retries - 1) throw error;\n console.log(`Intento ${i + 1} falló, intentando de nuevo...`);\n }\n }\n}\n\n\nCuándo usarlo: cualquier operación con recursos externos (API, bases de datos, sistema de archivos en Node.js). async/await es tu herramienta principal para código asíncrono.
Gorutinas en JavaScript: ¿mito o realidad?
\n\nEl término "gorutina" proviene de Go y se refiere a un hilo de ejecución ligero. En JavaScript no hay gorutinas nativas, pero existen generadores (Generator Functions) y Web Workers que pueden imitar un comportamiento similar. Los generadores permiten pausar y reanudar la ejecución de una función, lo que da control sobre el flujo de ejecución.
\n\nGeneradores como base de la concurrencia
\n\nfunction* taskGenerator() {\n console.log('Iniciando tarea 1');\n yield 'Tarea 1 completada';\n console.log('Iniciando tarea 2');\n yield 'Tarea 2 completada';\n console.log('Iniciando tarea 3');\n yield 'Tarea 3 completada';\n}\n\nconst iterator = taskGenerator();\nconsole.log(iterator.next().value); // Ejecuta la tarea 1\n// En este momento se puede hacer otra cosa\nconsole.log(iterator.next().value); // Ejecuta la tarea 2\nconsol