Está en la página 1de 12

Ejecución sincrónica vs.

ejecución asincrónica
Teoría

Introducción
Como ya vimos en el primer apunte de manejo de archivos, NodeJS nos ofrece múltiples
funciones para realizar operaciones sobre archivos con mucha facilidad. Ya hemos visto las
versiones ​sincrónicas​ y nos ha quedado pendiente ver las ​asincrónicas​. Pero ¿Qué significa
que una función sea ​asincrónica​? Veamos un poco de qué se trata.

Ejecución sincrónica
Siempre que escribamos más de una instrucción en un programa, acostumbramos esperar que
las instrucciones se ejecuten comenzando desde la primera línea, una por una, de arriba hacia
abajo, hasta llegar al final del bloque de código. En el caso de que una de esas instrucciones
sea una llamada a otra función, el orden de ejecución se pausa, y se procede a ejecutar las
instrucciones dentro de esa función. Sólo una vez ejecutadas todas las instrucciones de esa
función, es que el programa retomará con el flujo de instrucciones que venía ejecutando antes.

En todo momento, ​sólo se están ejecutando las instrucciones de una sola de las
funciones​ a la vez. O sea, debe finalizar una función para poder continuar con la otra.

Siguiendo esta idea, el fin de una función marca el inicio de la siguiente, y el fin de ésta, el
inicio de la que le sigue, y así sucesivamente, describiendo una secuencia que ocurre en ​una
única línea de tiempo​.

A está forma de ejecución se la conoce como ​sincrónica​.

Ejemplo

function​ ​funA​(){
​console​.log(​1​)
funB()
​console​.log(​2​)
}

function​ ​funB​(){
​console​.log(​3​)
funC()
​console​.log(​4​)
}
function​ ​funC​(){
​console​.log(​5​)
}

Al ejecutar la función ​funA()​ se muestra lo siguiente por pantalla:

1
3
5
4
2

Comportamiento de una función: bloqueante vs no-bloqueante


Cuando alguna de las instrucciones dentro de una función intente acceder a un recurso que se
encuentre fuera del programa (por ejemplo, enviar un mensaje por la red, o leer un archivo del
disco) nos encontraremos con dos maneras distintas de hacerlo: en forma bloqueante, o en
forma no-bloqueante (​blocking​ o ​non-blocking​).

Operaciones bloqueantes
Este tipo de operaciones permiten que el programa se comporte de la manera más intuitiva, es
decir, siguiendo las reglas establecidas en el punto anterior (ejecución sincrónica).

De esta manera si quisiéramos, por ejemplo, escribir un texto dentro de un archivo, podríamos
hacer lo siguiente:

import​ fs ​from​ ​'fs'

const​ arch = ​'f1.txt'


const​ texto = ​'hola mundo'

console​.log(​'comenzando...'​)

fs.writeFileSync(arch, texto)
console​.log(​`​${arch}​ grabado con éxito`​)

console​.log(​`finalizado`​)
Si lo ejecutamos, obtendremos un resultado similar a éste:

comenzando...
f1.txt grabado con éxito
finalizado

Operaciones no-bloqueantes
Ahora bien, en algunos casos esperar a que una operación termine para iniciar la siguiente
puede no ser la mejor idea, ya que esto podría causar grandes demoras en la ejecución del
programa. Es por eso que NodeJS ofrece una segunda opción: las operaciones no
bloqueantes.
Este tipo de operaciones permite que, una vez iniciadas, el programa pueda continuar con la
siguiente instrucción, sin esperar a que finalice la anterior. O sea, permite la ejecución de varias
operaciones ​en paralelo​, sucediendo al mismo tiempo. A este tipo de ejecución se la conoce
como ​asincrónica​.

Ejecución asincrónica
Para poder usar funciones que realicen operaciones no bloqueantes debemos aprender a
usarlas adecuadamente, y no generar efectos adversos en forma accidental.

Cuando se trata de código que se ejecuta en forma sincrónica, establecer el orden de ejecución
se vuelve tan fácil como decidir qué instrucción escribir primero. Sin embargo, cuando se trata
de ejecución asincrónica, sólo sabemos en qué orden comenzarán su ejecución las
instrucciones, pero no sabemos en qué momento ni en qué orden terminarán de ejecutarse.
Veamos el siguiente ejemplo, utilizando la versión no-bloqueante de la función de escritura en
archivos:

import​ fs ​from​ ​'fs'

const​ arch = ​'f1.txt'


const​ texto = ​'hola mundo'

console​.log(​'comenzando...'​)

fs.writeFile(arch, texto, () => {


​console​.log(​`​${arch}​ grabado con éxito`​)
})

console​.log(​`finalizado`​)
Si lo ejecutamos, obtendremos lo siguiente:

comenzando...
finalizado
f1.txt grabado con éxito

Qué pasó acá? Bueno, la explicación corta es la siguiente:


Dado que la operación de escritura es “no-bloqueante”, no bloquea la ejecución normal del
programa (no frena la ejecución de las demás instrucciones hasta haberse completado), y
permite que el programa se siga ejecutando. Entonces, la ejecución de la operación de
escritura “comienza” e inmediatamente cede el control a la siguiente instrucción, que escribe
por pantalla el mensaje de finalización. Finalmente, cuando la operación de escritura termina,
entonces ejecuta el callback, que en caso de no haberse disparado algún error, informará por
pantalla que la escritura se realizó con éxito.

Qué podemos hacer para evitar este comportamiento inesperado?


La solución es anidando las instrucciones dentro de los callbacks, de la siguiente manera:

...

fs.writeFile(arch, texto, () => {


​console​.log(​`​${arch}​ grabado con éxito`​)
​console​.log(​`finalizado`​)
})

Esto funciona porque lo (único) que podemos controlar en este tipo de operaciones es que el
callback ​siempre​ se ejecuta ​luego de finalizar​ todas las demás instrucciones involucradas en
ese llamado.

Combinar comportamientos sincrónicos y asincrónicos, para exprimir al máximo el potencial del


lenguaje, es un tema que abordaremos en las próximas clases.

Si quieren conocer más en profundidad sobre este tema (tan complejo como interesante!) el
cual no abordaremos en más detalle durante la cursada, pueden buscar sobre el “event loop”,
que es el sistema que administra el orden en que se ejecutan las instrucciones en NodeJS.
Manejo de Archivos (2da parte): forma asincrónica
Teoría

Introducción
Ya vimos que NodeJS ofrece un módulo que nos permite trabajar sobre el sistema de archivos
de nuestro sistema operativo, ya aprendimos sobre las operaciones más comunes que solemos
realizar sobre archivos, y ya vimos cómo realizarlas en forma sincrónica. Nos resta entonces
revisar sus versiones asincrónicas.

Notarán que todas las funciones de este apunte tienen el mismo nombre que sus versiones
sincrónicas, pero sin la palabra “Sync” al final, y en cambio, todas reciben un nuevo último
parámetro: un callback.

En este caso, la mayoría de los callbacks siguen la convención de que el callback deberá
recibir un primer parámetro destinado al error (si lo hubiere) para saber cómo manejarlo.

Algunos callbacks requieren además un segundo parámetro, en caso de que la función en


cuestión devuelva algún resultado, para indicar qué hacer con el mismo.

Dado que estas funciones poseen un parámetro que nos permite elegir cómo manejar los
errores que pueden surgir de su ejecución, no será necesario ejecutarlas utilizando ​try /
catch​.

Leer un archivo
Para leer un archivo usaremos la función ​readFile(ruta, encoding, callback)​. Recibe los
mismos parámetros que su versión sincrónica, más el callback.

Al igual que su versión sincrónica, la función se encarga internamente de abrir y cerrar el


archivo una vez finalizado su uso.

Ejemplo de uso:

fs.readFile(​'/ruta/al/archivo'​, ​'utf-8'​, (error, contenido) => {


​if​ (error) {
​// hubo un error, no pude leerlo, hacer algo!
} ​else​ {
​// en este punto del código, puedo acceder a todo el contenido
​// del archivo a través de la variable "contenido".
​console​.log(contenido)
}
})

Sobreescribir un archivo
Para escribir un archivo usaremos la función ​writeFile(ruta, datos, callback)​. Recibe
los mismos parámetros que su versión sincrónica, más el callback, que en este caso, solo
precisa recibir un parámetro para manejar algún eventual error.

Al igual que su versión sincrónica, la función se encarga internamente de abrir y cerrar el


archivo una vez finalizado su uso.

Ejemplo de uso:

fs.writeFile(​'/ruta/al/archivo'​, ​'TEXTO DE PRUEBA\n'​, error => {


​if​ (error) {
​// hubo un error, no pude sobreescribirlo, hacer algo!
} ​else​ {
​// no hubo errores, hacer algo (opcional)
​console​.log(​'guardado!'​)
}
})

Agregar contenidos a un archivo


Para agregar contenidos a un archivo usaremos la función append​File(ruta, datos,
callback)​. Recibe los mismos parámetros que su versión sincrónica, más el callback, que en
este caso, solo precisa recibir un parámetro para manejar algún eventual error.

Al igual que su versión sincrónica, la función se encarga internamente de abrir y cerrar el


archivo una vez finalizado su uso.

Ejemplo de uso:

fs.appendFile(​'/ruta/al/archivo'​, ​'TEXTO A AGREGAR\n'​, error => {


​if​ (error) {
​// hubo un error, no pude agregarlo, hacer algo!
} ​else​ {
​// no hubo errores, hacer algo (opcional)
​console​.log(​'guardado!'​)
}
})
Renombrar un archivo
Para escribir un archivo usaremos la función ​rename(rutaVieja, rutaNueva, callback)​.
Recibe los mismos parámetros que su versión sincrónica, más el callback, que en este caso,
solo precisa recibir un parámetro para manejar algún eventual error.

Ejemplo de uso:

fs.rename(rutaVieja, rutaNueva, error => {


​if​ (error) {
​// hubo un error, no pude renombrarlo, hacer algo!
} ​else​ {
​// no hubo errores, hacer algo (opcional)
​console​.log(​'renombrado!'​)
}
})

Borrar un archivo
Para borrar un archivo usaremos la función ​unlink(ruta, callback)​. El mismo parámetro
que su versión sincrónica, más el callback, que en este caso, solo precisa recibir un parámetro
para manejar algún eventual error.

Ejemplo de uso:

fs.unlink(ruta, error => {


​if​ (error) {
​// hubo un error, no pude borrarlo, hacer algo!
} ​else​ {
​// no hubo errores, hacer algo (opcional)
​console​.log(​'borrado!'​)
}
})

Crear una carpeta


Para crear una carpeta usaremos la función ​mkdir(ruta, callback)​. Recibe el mismo
parámetro que su versión sincrónica, más el callback, que en este caso, solo precisa recibir un
parámetro para manejar algún eventual error.
Ejemplo de uso:

fs.mkdir(ruta, error => {


​if​ (error) {
​// hubo un error, no pude crear la carpeta! hacer algo!
} ​else​ {
​// no hubo errores, hacer algo (opcional)
​console​.log(​'carpeta creada!'​)
}
})

Leer el contenido de una carpeta


Para obtener los nombres de los archivos y carpetas que se encuentran dentro de una carpeta
usaremos la función ​readdir(ruta, callback)​. Recibe el mismo parámetro que su versión
sincrónica, más el callback.

Ejemplo de uso:

fs.readdir(ruta, (error, nombres) => {


​if​ (error) {
​// hubo un error, no pude leer la carpeta! hacer algo!
} ​else​ {
​// hacer algo con los nombres!
​console​.log(nombres)
}
})
Manejo de Archivos (3ra parte): promesas
Teoría

Introducción
Ya vimos que NodeJS ofrece el módulo fs que nos permite operar tanto de forma sincrónica
como asincrónica. Así mismo, dentro del paradigma asincrónico, inicialmente ofrecía funciones
que reciben un callback para manejar el asincronismo. Con la llegada de las promesas, JS
agregó un módulo dentro de fs, que añade versiones de las funciones asincrónicas, pero que
en lugar de recibir callbacks, operan mediante promesas. Veremos aquí algunos ejemplos con
las mismas funciones sobre las que venimos trabajando. Inicialmente, las promesas se usaron
con su sintaxis nativa, y luego se agregó en una versión posterior una sintaxis simplificada
utilizando las (entonces) nuevas palabras reservadas “async” y “await”. Nos concentraremos
principalmente en está última.

Para no ser excesivamente repetitivo, solamente mostraré un par de ejemplos. El resto


de las funciones se puede deducir con facilidad. Caso contrario, dirigirse a la
documentación oficial, o consultar con el docente.

Promesas con su sintaxis simplificada (Async/Await):

Leer un archivo
Para leer un archivo usaremos la función ​readFile(ruta, encoding)​. Recibe los mismos
parámetros que su versión sincrónica.

Al igual que su versión sincrónica, la función se encarga internamente de abrir y cerrar el


archivo una vez finalizado su uso.

Ejemplo de uso:

async function​ ​fun​() {


try {
​const​ contenido = ​await ​fs.readFile(​'/ruta/al/archivo'​, ​'utf-8'​)
​console​.log(contenido)
catch (err) {
​// hubo un error, no pude leerlo, hacer algo!
}
}
En el caso de querer hacer algo con la variable fuera del bloque try/catch, la declaración
debería hacerse fuera del mismo.

Recordar que debemos anteponer la palabra “await” al llamado a la función para que ésta se
comporte de manera bloqueante. Si se omitiera la palabra “await” la instrucción
console.log(contenido) se ejecutaría ANTES de que a la variable contenido se le asigne el
resultado de la operación de lectura del archivo!

Recordar también que la palabra “await” puede usarse ÚNICAMENTE dentro de una función de
tipo “async”

Dado que estas funciones ya no poseen un parámetro que nos permite elegir cómo manejar los
errores que pueden surgir de su ejecución, vuelve a ser necesario ejecutarlas utilizando ​try /
catch ​!

Sobreescribir un archivo
Para escribir un archivo usaremos la función ​writeFile(ruta, datos)​. Recibe los mismos
parámetros que su versión sincrónica, más el callback, que en este caso, solo precisa recibir un
parámetro para manejar algún eventual error.

Al igual que su versión sincrónica, la función se encarga internamente de abrir y cerrar el


archivo una vez finalizado su uso.

Ejemplo de uso:

async function​ ​fun​() {


try {
​await ​fs.writeFile(​'/ruta/al/archivo'​, ​'TEXTO DE PRUEBA\n'​)
​console​.log(​'guardado!'​)
catch (err) {
​// hubo un error, no pude escribirlo, hacer algo!
}
}

Agregar contenidos a un archivo


Para agregar contenidos a un archivo usaremos la función append​File(ruta, datos,
callback)​. Recibe los mismos parámetros que su versión sincrónica, más el callback, que en
este caso, solo precisa recibir un parámetro para manejar algún eventual error.

Al igual que su versión sincrónica, la función se encarga internamente de abrir y cerrar el


archivo una vez finalizado su uso.
Ejemplo de uso:

async function​ ​fun​() {


try {
​await ​fs.appendFile(​'/ruta/al/archivo'​, ​'TEXTO DE PRUEBA\n'​)
​console​.log(​'agregado!'​)
catch (err) {
​// hubo un error, no pude agregarlo, hacer algo!
}
}

Renombrar un archivo
Para escribir un archivo usaremos la función ​rename(rutaVieja, rutaNueva, callback)​.
Recibe los mismos parámetros que su versión sincrónica, más el callback, que en este caso,
solo precisa recibir un parámetro para manejar algún eventual error.

Ejemplo de uso:

async function​ ​fun​(rutaVieja, rutaNueva) {


try {
​await ​fs.rename(rutaVieja, rutaNueva)
​console​.log(​'renombrado!'​)
catch (err) {
​// hubo un error, no pude renombrarlo, hacer algo!
}
}
Manejo de Archivos (Asincrónico)
Práctica

Se tiene el siguiente requerimiento funcional:


● Dada la ruta de un directorio origen, y una segunda ruta destino, realizar un backup del
primer directorio, en la segunda ruta provista.
● Si el directorio origen no existe, indicarlo mediante un error, e interrumpir la ejecución.
● Si el directorio destino no existe, crearlo. Sólo se debe crear si hay algún archivo para
copiar.
● Al realizar el backup, sólo deben copiarse los archivos de tipo ‘documento’. Para ello,
solo consideraremos documentos a aquellos archivos que posean una extensión (o sea,
que terminan en ‘.algunaExtension’). Adicionalmente, filtraremos también aquellos
archivos cuyo nombre comience con “.” (punto) ya que esta nomenclatura suele
reservarse para archivos ocultos.
● Si alguna de las copias falla, eso no debe interferir con la ejecución de la copia de las
demás.

1) Desarrollar un programa que realice lo pedido, procesando los archivos en forma


sincrónica.

2) Realizar el mismo programa, pero que funcione en forma asincrónica, utilizando


callbacks

3) Realizar el mismo programa, pero utilizando Promises, usando la sintaxis con


async/await.

4) Para pensar y comentar:


¿Encontrás alguna similitud entre las distintas variantes del programa desarrollado? ¿Se te
ocurren ventajas y desventajas de cada solución? Discutir.

También podría gustarte