Concurrencia

Jose Lopez
Aug 28, 2017 · 10 min read

Antes de comenzar, Que es Concurrencia?

En un contexto general de programación, concurrencia es la habilidad para ejecutar dos o más procesos computacionales simultáneamente mientras se comparten recursos. Estos procesos son llamados “hilos”, los hilos pueden compartir un simple procedimiento o pueden distribuirlos a través de una red y sincronizarlo más tarde. La comunicación entre procesos paralelos son usualmente explícitos, pasandolos a través de mensajes o compartiendo variables.

Aunque JavaScript no tiene una verdadera Concurrencia, es posible emular sus efectos a través del uso de funciones como: setInterval(), setTimeout(), o la versión asincrona de XMLHttpRequest(). JavaScript viene por default con un modelo de Concurrencia basado en “loop de eventos”, algo distinto a otros modelos de Concurrencia, este loop de eventos puede evaluar los programas continuamente para ver los mensajes de eventos entrantes al proceso, un simple hilo significa que hay un solo loop de eventos por tiempo de ejecución. el Loop de Eventos de JavaScript esta profundamente relacionado con dos conceptos: Ejecutar hasta completar (run-to-completion) y Sin Bloqueo (I/O).

Ejecutar hasta completar (run-to-completation)

El Loop de Eventos es diseñado como entorno para Ejecutar hasta completar. Es decir que una vez que JavaScript inicia a ejecutar una tarea, esta no puede ser interrumpida hasta que se complete. Sin Ejecutar hasta completar, no podriamos saber sobre el estado del objeto porque no podemos en ese caso acceder desde afuera de el Loop de Eventos.

Vamos a ver un simple ejemplo para entender este concepto:

En el ejemplo vemos como se imprime el primer log, luego el último log fuera del Loop de Eventos, y por último el log dentro del Loop de Eventos con la función setTimeout(), esta función toma como parámetro otra función (callback) y los milisegundos (el tiempo para que se dispare el callback), observamos que el log “Estado terminado” se imprime primero que el log que esta dentro de setTimeout(), y esto es porque JavaScript va agregando a una cola de mensajes para procesar cada tarea, entonces al llamar setTimeout() va agregando un mensaje a la cola dependiendo el tiempo (segundo parámetro). En este caso tenemos dos mensajes en la cola (“Estado fuera del Loop de Eventos!”) y (“Estado terminado”), setTimeout() espera a que los otros mensajes sean procesados, si no existe ningún mensaje en la cola el mensaje se procesa al instante.

Sin Bloqueo (I/O)

Hablamos del Sin Bloqueo (I/O) como una arquitectura asincrónica, lo contrario a Bloqueo (Ejecutar hasta completar), en vez de esperar hasta que se complete la tarea, esta se ejecuta inmediatamente, es decir que cualquier tarea de corto o largo plazo para finalizar, por ejemplo como puede ser procesar un archivo, comunicarse a una API via HTTP o hacer consultas a una Base de Datos, son peticiones que se retornan cuando los resultados estan completados, y estos resultados se retornan via un callback.

Callbacks

La función callback puede ser invocada cuando la operación es completada y así manipular peticiones adicionales, esto es fundamental en el Loop de Eventos de Nodejs ya que su arquitectura es totalmente asíncrona. Node opera de forma asíncrona via el Loop de Eventos y Callbacks, es decir el Loop de Eventos en Node no es nada más que llamar e invocar eventos en el tiempo adecuado. En Node, una función Callback es el controlador de eventos.

Vamos a ver un ejemplo con Nodejs para aclarar el concepto:

Voy a explicarlo por partes: primero importamos dos módulos de Nodejs (http y fs), con el módulo http creamos un servidor con el método createServer(), este método toma como argumento un callback, este callback tiene dos argumentos que son req (petición) y res (respuesta), dentro de la declaración de este callback usamos el módulo fs y su método readFile, este toma tres argumentos: el archivo que vamos a procesar (en este caso hello.txt), el segundo argumento es la codificación_, y por último el callback con dos argumentos: el error y la lectura del archivo. Dentro de la declaración del callback de readFile(), configuramos el head del http y le decimos que el tipo de contenido que vamos a servir como respuesta es de texto plano (text/plain), luego verificamos si hay un error en el proceso, si lo hay respondemos File not found, de lo contrario el archivo se proceso exitosamente y servimos el archivo al cliente. para iniciar el servidor el módulo http necesita el método listen() tomando como argumentos el puerto (en este caso el puerto 3000) y un callback que dentro de su declaración solo mostramos que el servidor esta corriendo. Como hemos visto en este ejemplo todos los métodos toman su respectivo callback para ejecutar la tarea inmediatamente, pero que pasa si tenemos un programa mucho más complejo, que despues de una tarea queremos ejecutar otra y otra seguidamente, seria algo como esto:

Para nada agradable, termina siendo mucho código engorroso, dificil de tratar y leer cuando se trata de trabajar con un equipo completo, esto conocido como el Callback Hell. Para evitar el Callback Hell hacemos el uso de Promesas, del cual sera el siguiente tema a tratar.

Promesas

El objetivo de las Promesas es simplificar la asincronia en JavaScript, manejando ordenes de ejecuciones a traves de una serie de pasos y manejar cualquier error asincrono que se dispare. Con los Promises evitamos el uso de los callbacks y así organizar el codigo asíncrono, mucho más facil de leer y mantener.

Una Promesa simplemente es un objeto que retorna un valor asíncrono, es decir: el valor de una operación asíncrona, tal como el resultado de una peticion HTTP o la lectura de un archivo en disco. Cuando una función asíncrona es llamada puede inmediatamente retornar una objeto de Promesas, usando ese objeto, puedes invocar callbacks que puedan ejecutar cuando la operación es exitosa o cuando ocurre un error.

vamos a tomar el ejemplo anterior de los Callbacks:

Ya sabemos lo que hace este programa, vamos a usarlo de nuevo pero en vez de Callbacks vamos a usar Promesas para manejar los procesos asíncronos.

Vamos por pasos, en Javascript para retornar una Promesa necesitamos un contructor/función global llamado Promise, este nos facilita toda la funcionalidad para las Promesas. Ahora fijandonos en el ejemplo, en la declaración de la función readFile creamos y retornamos una nueva Promesa, cuando el Promise lo usamos como constructor le pasamos un callback, este callback toma dos argumentos que son: el resolvedor (resolve) y el rechazador (reject) de las cuales ambos son funciones que se usan manipular la Promesa cuando se ejecute, así como suena claro: el resolvedor (resolve) es llamado cuando la Promesa se cumple exitosamente, en este caso de nuestro ejemplo, el resolvedor se llama cuando se carga el archivo y el rechazador (reject) es llamado si el archivo no puede ser cargado. Entonces para tomar el valor que retorna la Promesa usamos el método then para recibir el valor, en el caso de nuestro ejemplo recibe el archivo (file) y lo mandamos al cliente como respuesta.

Estados

Los estados de una Promesa nos ayuda a manipular la operación representada y almacenada por una Promesa, los estados nos indica cuando inicio, si esta en progreso, si ya se completo o se ha parado y no puede completarse, estas condiciones son representadas por tres comunes estados:

  • Pendiente: La operación no ha podido iniciar o esta en progreso.
  • Completada: La operación esta completada.
  • Rechazada: La operación no puede ser completada.

Nos referimos a los estados Completada y Rechazada como exitosamente y error, el estado Pendiente puede ser diferente, porque una Promesa puede ser Completada con un error, y cuando una operación no puede ser Completada no regresa ningun error, en resumen: Completada es cuando la Promesa cumple su ejecución y retorna el valor exitosamente y Rechazada cuando la Promesa retorna un error.

Encadenamiento

Como hemos visto, los métodos then() y catch() retornan el valor de la Promesa y así podemos encadenar los métodos facilmente, en caso de encadenar otro método no es la misma Promesa que regresa, es creada y retornada una nueva Promesa. Vamos a retornar una nueva Promesa!

Bien, usando el mismo ejemplo pero en este hemos encadenado dos métodos más, cada then() regresa una nueva Promesa y toman el valor de la Promesa retornada en el anterior, es decir en cada paso enviamos y retornamos el valor al próximo paso, es una secuencia de pasos donde podemos sacarle provecho e ir transformando ese valor y enviarlo, funciona así como un “pipeline”. Entonces enfocándonos en el ejemplo: el primer paso tomamos el archivo (la Promesa Completada) concatenamos una cadena y la retornamos al siguiente paso, el segundo paso toma la nueva Promesa retornada (la cadena concatenada) transformamos la cadena a mayúsculas y la retornamos al siguiente paso, el tercer y último paso tomamos la nueva Promesa retornada del paso anterior, aqui enviamos el valor (la cadena concatenada y en mayúsculas) al navegador y vemos el resultado final THIS TEXT COME FROM THE HELLO.TXT: HELLO WORLD. Así podemos ir encadenando más métodos si es necesario, no existe ningún límite para encadenar métodos, en el caso de este ejemplo fue para mantenerlo simple.

Capturando Errores

En operaciones síncronas manejamos los errores con el típico try catch, igual pasa en las Promesas, pero en este caso son para manejar operaciones asincrónicas. El error retorna si el estado se cumple pero es Rechazado, es decir el método reject dentro del callback de un constructor Promise (o método estático), y el método catch nos permite capturar el error retornado por el reject. Vamos a visualizar este concepto en un ejemplo muy simple:

En este ejemplo retornamos el Promise con el método reject, con un error dentro para propuesta de este ejemplo. Encadenamos dos métodos then y estos nunca seran ejecutados, de lo contrario son ejecutados cuando la Promesa es Completada exitosamente, ahora en este caso la Promesa es Rechazada y pasa al método donde manipulamos el error retornado.

Ejecución Paralela

Puede que necesitemos ejecutar multiples tareas asincrónas, las Promesas nos permite correr multiples tareas en paralelo, es decir las tareas se ejecutan simultáneamente hasta que se cumpla la Promesa y envia a las secuencias (then y catch) tal y como lo venimos haciendo. Para ejecutar tareas en paralelo usamos el método all desde la API de Promise, este método toma como argumento un iterable (un array de valores) y retorna una Promesa cuando todas las promesas del iterable hayan sido cumplidas exitosamente o de lo contrario si alguna Promesa del iterable falla sera rechazada. Vamos a visualizar el concepto en dos ejemplos de código, uno sera cuando es cumplida todas las promesas del iterable y otro cuando sea rechazada.

El primer ejemplo sera cuando todas las Promesas son cumplidas.

En este ejemplo usamos axios para las peticiones http, tenemos cuatro funciones que regresan una Promesa. Cada función la agregamos a un array que le llamamos en este caso iterable, entonces el método all toma el iterable (array de promesas) y si todas las Promesas son cumplidas exitosamente retorna los resultados a then, de lo contrario la Promesa sera rechazada y enviada a catch.

Vamos a ver el otro ejemplo pero con una promesa rechazada pero mucho más simple.

Esta muy claro! si alguna de las promesas en el iterable es rechazada y pasa al catch.

Generadores

Normalmente cuando una función es invocada y retorna un valor, la declaración del return activa un nuevo contexto de ejecución y descarta el contexto anterior porque lo hemos retornado, es decir se envia al call stack y se asigna a la memoria. Los Generadores nos permiten evitar la asignación en memoria innecesariamente y así evitamos invocar el “recolector de basura” frecuentemente, a parte que también mantienen su propio estado. Los Generadores tienen su propia sintaxis, se declaran mediante una función (como siempre lo hemos hecho) y un asterisco seguido de la palabra clave function y antes del nombre de la función y se ve así function*, el cuerpo de la función se declara igual que una función normal pero cuando es invocada no se ejecuta inmediatamente y esto es porque devuelve un objeto iterador, no es necesario instanciar ya que los generadores mantienen su propio estado y ya estan instanciados, vamos a ver como se ve un Generador.

Declaramos el Generador como he comentado al comienzo, a diferencia de una función normal cambia dos cosas que son: el * después de la palabra reservada function e invocamos la función y encadenamos el método next, el método next simplemente recorre el objeto secuencialmente y devuelve un objeto con dos propiedades value y done.

Retornando valores con yield

Hemos visto en el ejemplo anterior que retornamos el valor del Generador con return, y trabaja sin ningún problema, pero el caso común para retornar valores de un Generador es usando yield. La palabra reservada yield es el return pero basado en Generadores, la diferencia es que yield puede controlar el invocador del Generador, cuando retornamos con yield la expresión puede ser pausada hasta que invoquemos el método next para pasar a la siguiente ejecución. El yield igual que return retorna un objeto iterable con dos propiedades value y done, la diferencia acá es que yield retorna done como falso porque el yield vuelve atrás y evalúa el Generador y hasta que no se retornen todos los valores done sera falso, de lo contrario donepasa a verdadero. Vamos a tomar un ejemplo muy simple retornando valores con yield.

Bien! primero declaramos el Generador como ya sabemos, usamos la palabra reservada yield para retorna los valores, luego invocamos el Generador usando el método next para pasar de secuencia, fijandonos en los valores que retornamos con yield (Hello, World y Generator) cada next nos retorna el objeto iterable con las dos propiedades value y done, el primer next nos retorna el primer valor y el proximo nos retorna el segundo valor y así sucesivamente en orden secuencial, el último next es cuando termina el Generador y nos retorna el value como undefined y el done como true.

Conclusión

JavaScript no es un lenguaje totalmente Concurrente, pero estos últimos años ha cambiado para bien con nuevas características desde ES 2015. Realmente llevamos muchos años tratando Concurrencia con JavaScript, desde aquellos comienzos de las peticiones http con Ajax. Las Promesas ya llevan un tiempo también, antes era posible con librerías externas ahora ya es nativo del lenguaje, los Generadores son otra nueva funcionalidad al lenguaje que cambia la forma en la que pensamos sobre Concurrencia en JavaScript.

elblogdejavascript

Blog de JavaScript en Espanol

)

Jose Lopez

Written by

Javascript Dev | Full Stack Dev

elblogdejavascript

Blog de JavaScript en Espanol

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade