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.
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.
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 🤓.
📣 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:
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
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
enviandolen
como las dos variables que recibemultiply
. - 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.
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...
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.
Leemos la función
multiply
Leemos la función
square
Leemos la función
printSquare
Por último, leemos
printSquare(4);
. Esta sintaxis nos indica que se esta ejecutando la funciónprintSuqare
con un valor de 4.
Tal como vimos, ni bien se ejecuta una función, en este caso
printSquare
, la agregamos al Call Stack
La primer linea
var squared = square(n)
ejecuta la funciónsquare
. Es por esto que se agregasquare
al Call Stack
En esta ocasión la función
square
llama amultiply
y esta función es agregada al Call Stack
A continuación, la función
multiply
retorna la múltiplicación dea * b
. Al salir de la función, la misma es removida del Call Stack
Ahora
square
retorna el valor de la ejecución de la funciónmultiply
. Nuevamente, la función es removida del Call Stack.
Ahora tenemos la variable
squared
con el valor de la ejecución desquare
. Pero, todavía no termino la ejecución de la funciónprintSquare
.
Se ejecuta la segunda linea de
printSquare
que imprime en la consola el valor desquared
. Esto se realiza con la ejecución de una función llamadalog
del objetoconsole
. Es por eso que agregamos dicha ejecución al Call Stack.
Ya impresa la variable
squared
removemosconsole.log
del Call Stack.
Terminada la ejecución de
printSquare
la removemos del Call Stack.
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.
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.
Como siempre, antes de su ejecución el Call Stack se encuentra vacío
Ni bien comenzamos con la ejecución un proceso
main
que representa a la ejecución es agregado al Call Stack.
Leemos la primer linea, ejecutamos la función
getUser(1)
y agregamos la misma al Call Stack.
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.
Nos desbloqueamos, ahora podemos sacar a
getUser(1)
del stack y continuar con la siguiente linea de código
Ahora ejecutamos
getUser(2)
y agregamos el mismo al Call Stack.
Otra vez nos bloqueamos ❌, no nos queda otra que esperar a qué
getUser(2)
termine de ejecutarse.
Terminamos de ejecutar
getUser(2)
y podemos sacarlo del Call Stack
Ejecutamos la función
getUser(3)
y la agregamos al Call Stack.
Una vez más nos bloqueamos ❌ y tenemos que esperar a qué
getUser(3)
termine de ejecutarse.
Imprimimos en la terminal la variable
foo
agregando la ejecución al Call Stack.
Imprimimos en la terminal la variable
bar
agregando la ejecución al Call Stack.
Imprimimos en la terminal la variable
baz
agregando la ejecución al Call Stack.
Finalizada la ejecución solo nos falta remover a
main
del Call Stack.
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.
En la siguiente imagen van a poder ver la prueba que ejecute en mi navegador. Voy a estar realizando los siguientes pasos:
- Presiono el botón de open. podremos ver qué se abre el mensaje en el Navegador.
- Presiono nuevamente el botón de open y podremos ver que se abre el mensaje en el Navegador.
- Presiono el botón de run y comienza la ejecución del código. Esto bloquea al Navegador ❌.
- Presionó 2 veces el botón de open, pero nada sucede. Es decir, no aparecen los mensajes del Navegador por haber apretado open.
- Termina la ejecución de run. Aparece el mensaje de Finish y luego los 2 mensajes por haber apretado open.
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 👋.