¿Que es programación asincronica?
Si buscamos su definición exacta en Wikipedia encontramos lo siguiente:
“Asincronía , en programación de computadoras, se refiere a la ocurrencia de eventos de forma independiente de la corriente principal del programa y las formas de lidiar con este tipo de eventos.”
Lamentablemente esta traducción es de parte de Google Translator debido a que no hay wiki en español para este tema. Su definición original en ingles es:
“Asynchrony, in computer programming, refers to the occurrence of events independently of the main program flow and ways to deal with such events.”
Básicamente nos dice que en la programación asincronica, no tenemos control sobre cuando los eventos ocurrirán en el transcurso de ejecución de nuestro programa. ¿Que eventos? Bueno, eventos como el “click” de un mouse, el presionar de alguna tecla o datos provenientes de algún lugar a través de la red. Sabemos como nuestro programa reaccionara a dichos eventos pero no sabemos cuando ocurrirán.
Programación Sincrónica vs Asincronica
Usualmente, cuando aprendemos programar, lo hacemos de forma sincrónica, es decir en orden cronológico:
var miVar1 = 1; var miVar2 = 2; var miVarResult = miVar1 + miVar2; console.log('Resultado: ', miVarResult);
Nada sorprendente en eso, una instrucción después de otra se irán ejecutando hasta el final. La mayoría de lenguajes de programación fueron diseñados de esta manera y muchos de nosotros crecimos acostumbrados a ellos. ¿Y es malo programar de esta forma? No, en lo absoluto. ¿Pero que pasa si nos encontramos con una instrucción, la cual debe de hacer una llamada a la base de datos o contactar a un servidor remoto y no sabemos cuanto tiempo demorara en contestar? Lenguajes muy útiles como Python, que a pesar que cuentan con librerías asincronicas, por diseño, es un lenguaje sincrónico. En el siguiente ejemplo escrito en Python, contactamos a un servidor para obtener un archivo:
import requests body = requests.get('http://server-lento.org/archivo.txt') print(body.text) print('Fin del programa!')
En este ejemplo, hacemos uso de la librería “requests”, que nos permite hacer llamadas http a cualquier dirección. Hacemos una llamada hipotética a un servidor que tardara demasiado en responder, el código al final del archivo, el cual imprimirá “Fin del programa!”, no sera ejecutado hasta que la llamada del servidor vuelva. Este código se ejecuta síncronamente y es fácil de entender. ¿Pero que pasaría si la llamada al servidor dura 5 minutos o mas? La ultima linea de código no sera ejecutada hasta después de este lapso de tiempo.
Ahora veamos el mismo ejemplo escrito en JavaScript, de forma asincronica:
var request = require('request'); request('http://server-lento.org/archivo.txt', function(err, resp, body) { console.log(body); }); console.log('Fin del programa?');
Igualmente, hacemos uso de una librería equivalente en JavaScript, con casi el mismo nombre, “request”. En este caso, el código al final del archivo es ejecutado inmediatamente después de hacer la llamada hacia el servidor, la cual imprime el texto “Fin del programa?”. Pero el programa no termina aquí realmente, el código dentro de la llamada, console.log(body);
se ejecutara hasta que la llamada al servidor vuelva.
Entonces volviendo al ejemplo con Python, lo que veríamos en la consola seria:
Contenido del archivo... Fin del programa!
En caso del ejemplo con JavaScript, seria:
Fin del programa? Contenido del archivo...
Plataformas de un solo hilo (Single Threaded Platforms)
Antes de proceder, hablemos un poco de historia. El hecho que la programación en JavaScript sea asincronica no es accidental. Allá por el año 1995, Netscape tenia la urgencia de contar con un lenguaje liviano, que se pudiera integrar a las paginas HTML. La web estaba arrancando y las conexiones eran bastante lentas, unos 56 Kbps eran lo máximo. Puesto que los recursos eran demasiado limitados en los clientes, mas las conexiones sumamente lentas, los browsers fueron diseñados para correr sobre un hilo de ejecución del procesador. Además la seguridad para ejecutar instrucciones totalmente ajenos al sistema operativo es critica, y todo lo que corra sobre el browser debe estar aislado totalmente, hacer esto sobre una plataforma multi hilos (multithreading) solo complica demasiado las cosas.
Lo que si considero accidental (desde mi punto de vista) fue el beneficio de crear plataformas “single thread” del lado del servidor, como nodejs (Node) por ejemplo. Diseñar sistemas concurrentes es extremadamente difícil, aunque muchos de nosotros es muy rara vez que necesitamos algo tan demandante, al encontrarnos con tal tarea y utilizar herramientas tradicionales como mecanismos de bloqueo (Lock Mechanisms) como en Java o C#, es una pesadilla.
Node hace toda esta labor apto para mortales, la idea es muy sencilla, en vez de atacar un problema complejo utilizando concurrencia, abriendo varios hilos de ejecución en el procesador, este se limita a correr toda ejecución sobre un solo hilo, sin bloqueos. Esto significa que cada vez que queremos accesar algún recurso (archivo o base de datos) fuera de nuestro control, un servidor remoto por ejemplo, este no bloqueara el programa sino que se ira a una cola de espera, mientras tanto, node permite ejecutar mas instrucciones. Cuando nuestra petición al recurso regresa, esta vuelve al hilo de ejecución principal y termina de hacer lo que ordenamos hacer. Por supuesto, hay maneras de crear nuevos hilos de ejecución (vía spawn entre otros) pero sera tema para otro día.
Esta nueva forma de diseñar sistemas pueda que de una impresión limitante. Por ejemplo, que pasa con los ordenadores potentes con 8 o mas núcleos? Se preguntaran… La respuesta esta en “clusters” o con otra denominación, “web farms”, lo cual permiten correr un sistema distribuido en varios computadores o nodos. Tradicionalmente cuando montamos un servidor web, y al tiempo este se queda corto de capacidad, lo normal es aumentar su memoria RAM, adicionar espacio de almacenamiento, e incluso agregar mas procesadores si es posible. A la vuelta de los años, si el servidor se queda corto de nuevo, volvemos hacer lo mismo, y así hasta mas no poder. Este tipo de crecimiento se llama “Escalabilidad Vertical” (Vertical Scaling) en cual nuestro único servidor va creciendo en capacidad. Si por el contrario, visualizamos un sistema como el conjunto de mini procesos o programas, podemos distribuir estos procesos entre núcleos, procesadores o incluso entre computadores (nodos). Este crecimiento se llama “Escalabilidad Horizontal” (Horizontal Scaling), la ventaja de esta modalidad es que el crecimiento es dinámico en el sentido que si queremos disminuir o aumentar la capacidad de nuestra arquitectura solo removemos o agregamos mas nodos. La capacidad a la que nuestra configuración puede crecer lo limita solo nuestro presupuesto, además los nodos pueden ser extremadamente baratos (un núcleo, poca memoria RAM) o complejos (varios núcleos, mucha memoria, etc.). Esta modalidad se ha vuelto tan popular que prácticamente es el estándar a la hora de arrendar infraestructura en la nube como Amazon AWS, Heroku o OpenShift, quitar o agregar nodos es cuestión de clicks. La idea de escalabilidad horizontal no exclusivo de Node, pero el hecho que solo podamos correr un programa por hilo de ejecución (o núcleo) hace que el diseño de sistemas se adapte perfectamente a este modelo de crecimiento.
Callbacks
Volviendo al tema, nos preguntamos porque en JavaScript es mas “habitual” desarrollar asincronicamente? La respuesta reside en sus callbacks, que son simplemente funciones. Cuando este lenguaje fue diseñado se tomo una decisión aparentemente superficial pero sumamente importante. Las funciones al igual que otros tipos primitivos (Strings, Numbers, etc.) son “ciudadanos de primera clase” (first class citizens), que significa que pueden almacenarse en variables y circular por todo el código como cualquier otro valor. ¿Y esto que? Veamos…
var miFun = function() { console.log('Hola funcion!'); }; // Correr en el futuro // despues de 5 segundos setTimeout(miFun, 5000);
En este ejemplo, la función es declarada y almacenada en la variable miFun
. La función setTimeout
es propia de JavaScript y lo que hace es ejecutar la función que le pasemos después del periodo de tiempo que le indiquemos, en este caso son 5000 milisegundos. Esta variable miFun
la podemos enviar a cualquier parte del programa que tenga acceso a ella, por ende, todo código que pueda utilizar esta variable puede ejecutar nuestra función.
La idea de almacenar una función en una variable y pasarla de arriba abajo, no es nada nueva, viene de muy antes, desde los anos 70’s, pero sorprendentemente se volvió popular solo durante este ultima década, y me atrevería a decir que fue gracias a la web y a JavaScript. La técnica de usar funciones como valor es tan poderosa que la idea ahora se encuentra en la mayoría de lenguajes dinámicos como Ruby o Python, o incluso en los estáticos como Java y C# con sus expresiones “lambdas” que fueron incorporadas hace algunos anos.
Las funciones no solo se pueden almacenar en variables, sino que se pueden también declarar en cualquier instancia y pasar como argumento a otra función. A esto, le denominamos funciones anónimas:
// Correr cada 5 segundos setInterval(function() { console.log('Hola function!'); }, 5000);
Aquí, le instruimos a setInterval
que ejecute nuestra función cada 5 segundos, debido a que no nos interesa reutilizar esta función en ninguna otra parte, la declaramos en el “mismo instante” que la pasamos como argumento a la función setInterval. De esta forma, le estamos instruyendo a nuestro programa que imprima en nuestra consola el texto “Hola función” periodicamente en el futuro sin bloquear su ejecución, si en caso el programa tuviera mas instrucciones.
Imaginemos que estamos construyendo una librería para nuestros clientes. Esta contara con una función para localizar el nombre de algún sujeto con solo saber su numero de identificación personal. Nuestra librería, internamente buscara en una larga lista de información y no sabemos cuanto demorara. Puesto que ella correrá en un hilo de ejecucion distinto al de nuestros clientes (o incluso en otra maquina), esta no los bloqueara pero de alguna manera debemos notificarles que nuestra función termino y localizo la información. Esto lo podemos lograr haciendo que nuestros clientes, además del # del ID, nos den una función por la cual les podamos señalar que hemos terminado, en pocas palabras un “callback”.
// mi-libreria.js module.exports = function(personalID, callback) { // Un largo proceso de busqueda, // eventualmente se localiza y se // almacena la info var nombre = 'Nombre Encontrado'; // Y se envia de vuelta callback(null, nombre); };
Aquí creamos un modulo y exportamos solamente una función. Pero esta función espera dos argumentos, uno es el ID del sujeto y el otro es el callback. La función después de hacer una larga labor llama al callback con la información solicitada.
Hay otro detalle en este ejemplo que debemos de poner atención y es a la hora de llamar al callback. Notaran que esta función se llama con dos argumentos también, el primero es null
y el segundo es propiamente el nombre. ¿Porque enviamos null
? Esta es una convención en Node, el primer argumento indica si hubo un error o no, el siguiente o el resto son propiamente del callback. Entonces enviamos null
porque todo acabo exitosamente, de lo contrario mandamos otro valor para indicar que algo malo paso.
// mi-libreria.js module.exports = function(personalID, callback) { // Un largo proceso de busqueda, // eventualmente.. Oh oh, hubo un error // Y se envia de vuelta callback(new Error('Algo malo sucedio!')); };
Ahora, supongamos que nosotros somos el cliente de esta librería. ¿Como la utilizamos? Sencillo:
const buscar = require('mi-libreria'); buscar('123456789', function(err, nombre) { if(err) { console.log(err.message); return; } console.log('Nombre es: ', nombre); }); console.log('Mas codigo aqui!');
Aquí llamamos a la única función que la librería ofrece, le pasamos el ID que queremos que busque, y luego nuestro callback. La siguiente linea se ejecuta inmediatamente e imprime “Mas código aqui!”. Al rato, cuando la función termina su búsqueda llama a nuestro callback y nuestro código se ejecuta. Primero verificamos con no halla sucedido algún error, de lo contrario se imprime el nombre del sujeto 🙂
Para concluir, solo quisiera agregar que el hecho de programar asincronicamente es muy sencillo pero la complejidad aparece cuando el programa va creciendo y se quiere mantener un orden cronológico de como se ejecutan las llamadas a otras librerías. Esto por supuesto se puede lograr utilizando solamente callbacks, pero el código tiende a volverse un sphagetti y es conveniente utilizar librerías para mitigar este desorden, librerías como “async” o utilizando “promises” (promesas) que rápidamente se esta volviendo la norma en JavaScript. A pesar de este contratiempo, el hecho de programar en este estilo es sumamente poderoso y no es casualidad que otros lenguajes y frameworks también lo estén empleando.
Comentarios recientes