Ariel Rey

Curso Básico de NodeJS - Parte 2 - El Call Stack

Hola 👋, mi nombre es Ari Rey y bienvenidos a esta segunda parte del Curso Básico de NodeJS sin ejecutar una linea de código. Antes de arrancar, quiero agradecerles todo el apoyo que me dieron en Twitter 👏. Al igual que con la Parte 1, pueden encontrar este mismo curso en mi canal de Youtube, y si les gusto, subscríbanse al canal y denle me gusta 👍 al video.

alt

Haciendo un resumen de lo visto en la Parte 1, podemos decir que:

NodeJS es un Runtime Environment que utiliza un modelo no bloqueante de comunicación de E/S mediante el uso de eventos. Es Single Thread, esto quiere decir que usa un solo hilo de ejecución sin bloquearse y esto es posible mediante el uso de un Event Loop.

Para poder llegar a esta definición y comprenderla, tuvimos que aprender los siguientes conceptos:

-[x] Runtime Environment y Runtime Engine -[x] El modelo bloqueante y no bloqueante -[x] Comunicación de Entrada / Salida ó Input / Output -[x] Hilos de Ejecución (Threads). Multihilos (Multithread) -[x] El proceso para obtener código de bajo nivel, tanto en NodeJS 🤩, como en JAVA 😑

V8

Antes de empezar, recordemos el gráfico en el cual represente a NodeJS como un Runtime Environment.

alt

Dentro del Runtime nos encontramos con el Runtime Engine (V8) y con Node APIS o Módulos

En esta ocasión vamos a hacer un zoom en el Runtime Engine V8. Ojo, un zoom super simplificado, ninguno (me incluyo 🤷‍♂️) es experto en la materia 🤓.

alt

📣 En principio vamos a estar hablando de V8 dentro del Navegador Chrome. Más adelante vamos a ver "qué cambia" entre el V8 del Navegador Chrome y el V8 que utiliza NodeJS

Dentro de V8 podemos encontrar al Heap y al Stack. ¿Qué son? Estos son conceptos super complicados de explicar. Lo que puedo decir es tanto el Heap como el Stack son espacios en memoria. Él Stack es una pila LIFO (Last in, first out / Último en entrar, primero en salir) y es manejada automáticamente, es decir, nosotros no nos tenemos que encargar de liberarla. En cambio él Heap es un espacio en memoria semi manejado por nosotros (V8) y el CPU, si no liberamos bien la memoria podemos ocasionar un memory leak 💥. Por nuestra salud mental, dejemos este tema acá 🤕.

Si miramos 👀 el código de V8 y buscamos los métodos setTimeout, setInterval ó los objetos DOM, Document, XMLHttpRequest no vamos a poder encontrarlos 🤯. Raro, ¿no?. Esto sucede ya que V8 no esta solo, si miramos la foto completa podemos observar lo siguiente:

alt

En esta imagen podemos ver que aparte del Motor V8 tenemos las Web APIs que son las funciones que nos proveé el Navegador (DOM, setTimeout, etc...), Callback Queue y el famoso Event Loop 😎.

El Call Stack

En la Parte 1 vimos que NodeJS es Single Thread, esto significa que tiene un solo Call Stack y que puede hacer una sola cosa a la vez

alt

Para que veamos de una manera simple y fácil cómo funciona el Call Stack, a continuación vamos a hacer una práctica mental (sin ejecutar código 😜) de cómo el siguiente ejemplo se representaría en un Call Stack.

function multiply(a, b) {
	return a * b;
}

function square(n) {
	return mutiply(n, n);
}

function printSquare(n) {
	var squared = square(n);
	console.log(squared);
}

printSquare(4);

El siguiente código declara 3 funciones:

  • multiply: Recibe dos variables (a, b) y retorna la multiplicación de ambas.
  • square: Recibe una variable (n) y retorna una llamada al método multiply enviandole n como las dos variables que recibe multiply.
  • printSquare: Recibe una variable (n), llama al método square con dicha variable e imprime el resultado.

Una vez declaradas estas 3 funciones, se llama al método printSquare. El resultado de dicha ejecución imprimirá en consola el cuadrado de 4, es decir, 4 * 4.

¿Cómo funciona el Call Stack?

A continuación vamos a simular la ejecución del código y representar los distintos estados que tiene el Call Stack.

alt

A la izquierda el código y a la derecha el Call Stack

Como les conté previamente, el Call Stack es una estructura tipo pila LIFO que graba donde estamos en la ejecución del código. Si ejecutamos una función esta se agrega al Call Stack, y si salimos de la función esta sale del Call Stack. Eso es todo lo que puede hacer. Continuemos...

alt

Si ejecutamos el siguiente código, vamos a tener una primer función llamada main que representa la ejecución del archivo en sí mismo. Entonces, la agregamos al Call Stack.

alt

Leemos la función multiply

alt

Leemos la función square

alt

Leemos la función printSquare

alt

Por último, leemos printSquare(4);. Esta sintaxis nos indica que se esta ejecutando la función printSuqare con un valor de 4.

alt

Tal como vimos, ni bien se ejecuta una función, en este caso printSquare, la agregamos al Call Stack

alt

La primer linea var squared = square(n) ejecuta la función square. Es por esto que se agrega square al Call Stack

alt

En esta ocasión la función square llama a multiply y esta función es agregada al Call Stack

alt

A continuación, la función multiply retorna la múltiplicación de a * b. Al salir de la función, la misma es removida del Call Stack

alt

Ahora square retorna el valor de la ejecución de la función multiply. Nuevamente, la función es removida del Call Stack.

alt

Ahora tenemos la variable squared con el valor de la ejecución de square. Pero, todavía no termino la ejecución de la función printSquare.

alt

Se ejecuta la segunda linea de printSquare que imprime en la consola el valor de squared. Esto se realiza con la ejecución de una función llamada log del objeto console. Es por eso que agregamos dicha ejecución al Call Stack.

alt

Ya impresa la variable squared removemos console.log del Call Stack.

alt

Terminada la ejecución de printSquare la removemos del Call Stack.

alt

Una vez terminada la ejecución de esta apliación, podemos sacar a main del Call Stack.

Simple 😎, ¿no?. Ahora, podremos preguntarnos, ¿Qué pasa cuando ejecutamos funciones que son lentas?.

Bloqueando el Call Stack

No existe una definición clara de que es bloqueante y qué no, simplemente es código que es lento. Por ejemplo, console.log no es lento, pero procesar imágenes es lento, leer archivos es lento. Son las cosas lentas ⏳ las que bloquean el Call Stack.

alt

Digamos que tenemos el siguiente ejemplo:

var foo = getUser(1);
var bar = getUser(2);
var baz = getUser(3);

console.log(foo);
console.log(bar);
console.log(baz);

El siguiente código realiza tres pedidos de usuarios mediante la función getUser que recibe el por parámetro el identificador del usuario a buscar. Luego, la información del usuario en las variables foo, bar, baz es impresa en consola. No voy a mostrar la implementación de la función de getUser, simplemente les voy a decir que lo que hace esta función es consumir un servicio en Internet que nos va a decir la información del usuario solicitada. Tengamos en cuenta que la respuesta de getUser no es inmediata, va a tomar un tiempo ⏳.

Volvamos a hacer el ejercicio de simular la ejecución del código y representarla en el Call Stack. En esta oportunidad, olvidémonos del asincronismo de Javascript, simulemos esta ejecución como si el código fuera sincrónico.

alt

Como siempre, antes de su ejecución el Call Stack se encuentra vacío

alt

Ni bien comenzamos con la ejecución un proceso main que representa a la ejecución es agregado al Call Stack.

alt

Leemos la primer linea, ejecutamos la función getUser(1) y agregamos la misma al Call Stack.

alt

Nos bloqueamos ❌, no nos queda otra que esperar a qué getUser(1) termine de ejecutarse. Recuerden que les dije que íbamos a ejecutar este código cómo si fuera sincrónico.

alt

Nos desbloqueamos, ahora podemos sacar a getUser(1) del stack y continuar con la siguiente linea de código

alt

Ahora ejecutamos getUser(2) y agregamos el mismo al Call Stack.

alt

Otra vez nos bloqueamos ❌, no nos queda otra que esperar a qué getUser(2) termine de ejecutarse.

alt

Terminamos de ejecutar getUser(2) y podemos sacarlo del Call Stack

alt

Ejecutamos la función getUser(3) y la agregamos al Call Stack.

alt

Una vez más nos bloqueamos ❌ y tenemos que esperar a qué getUser(3) termine de ejecutarse.

alt

Imprimimos en la terminal la variable foo agregando la ejecución al Call Stack.

alt

Imprimimos en la terminal la variable bar agregando la ejecución al Call Stack.

alt

Imprimimos en la terminal la variable baz agregando la ejecución al Call Stack.

alt

Finalizada la ejecución solo nos falta remover a main del Call Stack.

alt

Funciono, ¿no? 🤔. Entonces, ¿Cuál es el problema?. Cómo les dije en un principio, primero íbamos a empezar hablando de V8 dentro de un Navegador, como por ejemplo Chrome. Y, ¿qué pasa cuándo ejecutamos algo bloqueante dentro de un Navegador?.

Bloqueando el Navegador

A continuación voy a mostrarles un ejemplo de lo que sucede cuando se bloquea un Navegador. En el siguiente ejemplo vamos a simular la ejecución del código que estábamos viendo.

var foo = getUser(1);
var bar = getUser(2);
var baz = getUser(3);

Recuerden, el método getUser consulta un servicio y toma tiempo. En el siguiente ejemplo, nos vamos a encontrar con una página que cuenta con 2 botones:

  • Open: En el momento que presione el botón se va a mostrar un mensaje en el Navegador
  • Run: En el momento que presione el botón se va a ejecutar de manera sincrónica el pedido de información de los tres usuarios (getUser). Cada uno de esos pedidos demora dos segundos en total. Una vez terminada la ejecución se muestra un mensaje en el Navegador que dice Finish.

alt

En la siguiente imagen van a poder ver la prueba que ejecute en mi navegador. Voy a estar realizando los siguientes pasos:

  1. Presiono el botón de open. podremos ver qué se abre el mensaje en el Navegador.
  2. Presiono nuevamente el botón de open y podremos ver que se abre el mensaje en el Navegador.
  3. Presiono el botón de run y comienza la ejecución del código. Esto bloquea al Navegador ❌.
  4. Presionó 2 veces el botón de open, pero nada sucede. Es decir, no aparecen los mensajes del Navegador por haber apretado open.
  5. Termina la ejecución de run. Aparece el mensaje de Finish y luego los 2 mensajes por haber apretado open.

alt

En caso de que la imagen sea pequeña o quieran reproducir este ejemplo, puedo reproducirlo desde el siguiente link.

Pero, ¿cómo solucionamos esto?, como hacemos para que el navegador no se bloqueé. Esto, vamos a verlo en la siguiente parte del curso de NodeJS 😅.

Gracias

Gracias por haber leído esta segunda parte, si les gusto, recuerden seguirme en Twitter para estar actualizados 👋.