JavaScript es un lenguaje monohilo y, al mismo tiempo, también no bloqueante, asíncrono y concurrente. Este artículo le explicará cómo sucede.
Tiempo de ejecución
JavaScript es un lenguaje interpretado, no compilado. Esto significa que necesita un intérprete que convierta el lenguaje JS código a un código máquina. Existen varios tipos de intérpretes (conocidos como motores). Los más populares son V8 (Chrome), Quantum (Firefox) y WebKit (Safari). Por cierto, V8 también se utiliza en un popular runtime que no es de navegador, Node.js.
Cada motor contiene un montón de memoria, una pila de llamadas, un bucle de eventos, una cola de retrollamadas y una WebAPI con peticiones HTTP, temporizadores, eventos, etc., todo implementado a su manera para una interpretación más rápida y segura del código JS.
Arquitectura básica de ejecución JS. Autor: Alex Zlatkov
Hilo único
Un lenguaje monohilo es aquel que tiene una única pila de llamadas y un único montón de memoria. Esto significa que sólo ejecuta una cosa a la vez.
A pila es una región continua de memoria, que asigna un contexto local para cada función ejecutada.
A montón es una región mucho mayor, que almacena todo lo asignado dinámicamente.
A pila de llamadas es una estructura de datos que básicamente registra dónde nos encontramos en el programa.
Pila de llamadas
Escribamos un código sencillo y sigamos lo que ocurre en la pila de llamadas.
Como puede ver, las funciones se añaden a la pila, se ejecutan y posteriormente se eliminan. Es el llamado modo LIFO - Last In, First Out (último en entrar, primero en salir). Cada entrada de la pila de llamadas se denomina marco de pila.
El conocimiento de la pila de llamadas es útil para leer las trazas de la pila de errores. Generalmente, el motivo exacto del error se encuentra en la parte superior de la primera línea, aunque el orden de ejecución del código es ascendente.
A veces se puede tratar de un error popular, notificado por Se ha superado el tamaño máximo de la pila de llamadas. Es fácil conseguirlo utilizando la recursividad:
function foo() {
foo()
}
foo()
y nuestro navegador o terminal se congela. Cada navegador, incluso sus diferentes versiones, tiene un límite de tamaño de pila de llamadas diferente. En la gran mayoría de los casos, son suficientes y el problema debe buscarse en otro sitio.
Pila de llamadas bloqueada
He aquí un ejemplo de bloqueo del hilo JS. Intentemos leer un foo y un archivo bar utilizando el Nodo.js función sincrónica readFileSync.
Este es un GIF en bucle. Como ves, el motor JS espera hasta la primera llamada en readFileSync se ha completado. Pero esto no sucederá porque no hay foo por lo que nunca se llamará a la segunda función.
Comportamiento asíncrono
Sin embargo, JS también puede ser no-bloqueante y comportarse como si fuera multi-hilo. Esto significa que no espera la respuesta de una llamada a la API, eventos de E/S, etc., y puede continuar la ejecución del código. Esto es posible gracias a los motores JS que utilizan (bajo el capó) lenguajes multihilo reales, como C++ (Chrome) o Rust (Firefox). Ellos nos proporcionan la API Web bajo las capuchas de los navegadores o ex. I/O API bajo Node.js.
En el GIF anterior, podemos ver que la primera función es empujada a la pila de llamadas y Hola se ejecuta inmediatamente en la consola.
A continuación, llamamos al setTimeout proporcionada por la WebAPI del navegador. Va a la pila de llamadas y su devolución de llamada asíncrona foo va a la cola de WebApi, donde espera la llamada, programada para que ocurra después de 3 segundos.
Mientras tanto, el programa continúa el código y vemos Hola. No estoy bloqueado en la consola.
Una vez invocada, cada función de la cola WebAPI pasa a la carpeta Cola de devolución de llamada. Es donde las funciones esperan hasta que la pila de llamadas está vacía. Cuando esto ocurre, se mueven allí una a una.
Así, cuando nuestro setTimeout termina la cuenta atrás, nuestro foo va a la cola de retrollamadas, espera hasta que la pila de llamadas esté disponible, va allí, se ejecuta y vemos Hola desde callback asíncrono en la consola.
Bucle de eventos
La pregunta es, ¿cómo sabe el tiempo de ejecución que la pila de llamadas está vacía y cómo se invoca el evento en la cola de devolución de llamada? Conoce el bucle de eventos. Forma parte del motor JS. Este proceso comprueba constantemente si la pila de llamadas está vacía y, si lo está, controla si hay un evento en la cola de llamadas de retorno esperando a ser invocado.
Así es la magia entre bastidores.
Recapitulación de la teoría
Concurrencia y paralelismo
Concurrencia significa ejecutar varias tareas al mismo tiempo, pero no simultáneamente. Por ejemplo, dos tareas funcionan en periodos de tiempo que se solapan.
Paralelismo significa realizar dos o más tareas simultáneamente, por ejemplo, realizar varios cálculos al mismo tiempo.
Hilos y procesos
Hilos son una secuencia de ejecución de código que pueden ejecutarse independientemente unas de otras.
Proceso es una instancia de un programa en ejecución. Un programa puede tener varios procesos.
Sincrónico y asincrónico
En síncrono programación, las tareas se ejecutan una tras otra. Cada tarea espera a que se complete cualquier tarea anterior y se ejecuta solo entonces.
En asíncrono programación, cuando se ejecuta una tarea, puede cambiar a otra distinta sin esperar a que finalice la anterior.
Sincrónico y asincrónico en un entorno monohilo y multihilo
Sincrónico con un único hilo: Las tareas se ejecutan una tras otra. Cada tarea espera a que se ejecute la anterior.
Sincrónico con múltiples hilos: Las tareas se ejecutan en diferentes hilos pero esperan a que se ejecute cualquier otra tarea en cualquier otro hilo.
Asíncrono con un único hilo: Las tareas comienzan a ejecutarse sin esperar a que termine otra tarea. En un momento dado, sólo puede ejecutarse una única tarea.
Asíncrono con múltiples hilos: Las tareas se ejecutan en diferentes hilos sin esperar a que se completen otras tareas y terminan sus ejecuciones de forma independiente.
Clasificación JavaScript
Si consideramos cómo funcionan los motores JS bajo el capó, podemos clasificar JS como un lenguaje interpretado asíncrono y de un solo hilo. La palabra "interpretado" es muy importante porque significa que el lenguaje siempre dependerá del tiempo de ejecución y nunca será tan rápido como los lenguajes compilados con multihilo incorporado.
Cabe destacar que Node.js puede conseguir un multihilo real, siempre que cada hilo se inicie como un proceso independiente. Existen librerías para ello, pero Node.js incorpora una función llamada Hilos de trabajo.
Todos los GIF del bucle de eventos proceden del Lupa creada por Philip Roberts, donde puedes probar tus escenarios asíncronos.