Del yield al async/await
Diseño para todys
Concurrencia vs. paralelismo
Concurrencia implica hacer varias tareas en un lapso de tiempo fijo: lavé el auto, cociné, me bañé y cambié una lamparita. Está bueno que no lo intente hacer todo al mismo tiempo.
Concurrencia vs. paralelismo
Paralelismo implica hacer múltiples tareas simultáneamente. Podríamos escuchar la radio y manejar, o tener una conversación mientras cocinamos.
Java Virtual Machine: múltiples hilos
La Virtual Machine de Java trabaja con múltiples hilos, permitiendo paralelizar ciertas tareas.
Hilo 1
Hilo 2
Hilo 3
Java Virtual Machine: múltiples hilos (Ventajas)
Trabajar con una arquitectura multithreading permite que nuestro código pueda pensarse de manera sincrónica, lo que es más fácil de implementar.
Hilo 1
Hilo 2
Hilo 3
código sincrónico
código sincrónico
código sincrónico
Java Virtual Machine: múltiples hilos (Desventajas)
La cantidad de hilos tiene un límite, y cuando llegamos a ese límite se producen demoras. Por lo general los hilos comparten información en común (base de datos, shared memory, etc.), lo que necesita mecanismos de sincronización para evitar condiciones de carrera o deadlocks.
Hilo 1
Hilo 2
Hilo 3
Información compartida
Java Virtual Machine: múltiples hilos - ejemplo
¿Qué pasa cuando ejecutan en dos hilos diferentes el código asociado al mismo objeto?
class Cliente {
var saldo = 0
fun pagar(cuanto: Int) {
val nuevoSaldo = saldo - cuanto
saldo = nuevoSaldo
}
fun facturar(cuanto: Int) {
saldo = saldo + cuanto
}
}
class Cliente {
var saldo = 0
fun pagar(cuanto: Int) {
val nuevoSaldo = saldo - cuanto
saldo = nuevoSaldo
}
fun facturar(cuanto: Int) {
saldo = saldo + cuanto
}
}
objeto cliente, saldo = 200
Hilo 1: cliente.pagar(50)
Hilo 1
Hilo 2
Java Virtual Machine: múltiples hilos - ejemplo
¿Qué pasa cuando ejecutan en dos hilos diferentes el código asociado al mismo objeto?
class Cliente {
var saldo = 0
fun pagar(cuanto: Int) {
val nuevoSaldo = saldo - cuanto
saldo = nuevoSaldo
}
fun facturar(cuanto: Int) {
saldo = saldo + cuanto
}
}
class Cliente {
var saldo = 0
fun pagar(cuanto: Int) {
val nuevoSaldo = saldo - cuanto
saldo = nuevoSaldo
}
fun facturar(cuanto: Int) {
saldo = saldo + cuanto
}
}
objeto cliente, saldo = 200
Hilo 1: cliente.pagar(50)
nuevoSaldo = 150
Hilo 2: cliente.facturar(70)
Hilo 1
Hilo 2
Java Virtual Machine: múltiples hilos - ejemplo
¿Qué pasa cuando ejecutan en dos hilos diferentes el código asociado al mismo objeto?
class Cliente {
var saldo = 0
fun pagar(cuanto: Int) {
val nuevoSaldo = saldo - cuanto
saldo = nuevoSaldo
}
fun facturar(cuanto: Int) {
saldo = saldo + cuanto
}
}
class Cliente {
var saldo = 0
fun pagar(cuanto: Int) {
val nuevoSaldo = saldo - cuanto
saldo = nuevoSaldo
}
fun facturar(cuanto: Int) {
saldo = saldo + cuanto
}
}
objeto cliente, saldo = 200
Hilo 1: cliente.pagar(50)
nuevoSaldo = 150
Hilo 2: cliente.facturar(70)
Hilo 2: saldo = 270
Hilo 1
Hilo 2
Java Virtual Machine: múltiples hilos - ejemplo
Podemos llegar a una race condition...
class Cliente {
var saldo = 0
fun pagar(cuanto: Int) {
val nuevoSaldo = saldo - cuanto
saldo = nuevoSaldo
}
fun facturar(cuanto: Int) {
saldo = saldo + cuanto
}
}
class Cliente {
var saldo = 0
fun pagar(cuanto: Int) {
val nuevoSaldo = saldo - cuanto
saldo = nuevoSaldo
}
fun facturar(cuanto: Int) {
saldo = saldo + cuanto
}
}
objeto cliente, saldo = 200
Hilo 1: cliente.pagar(50)
nuevoSaldo = 150
Hilo 2: cliente.facturar(70)
Hilo 2: saldo = 270
Hilo 1: nuevoSaldo = 150
y vamos a actualizar el saldo a... 150!!!
Hilo 1
Hilo 2
Java Virtual Machine: múltiples hilos - ejemplo
...y tenemos que resolverlo con algún mecanismo de sincronización
class Cliente {
var saldo = 0
@Synchronized
fun pagar(cuanto: Int) {
val nuevoSaldo = saldo - cuanto
saldo = nuevoSaldo
}
@Synchronized
fun facturar(cuanto: Int) { saldo = saldo + cuanto }
}
cliente
JS Virtual Machine: arquitectura single threading
En una arquitectura con un solo hilo (principal), solo podemos ejecutar un proceso a la vez.
orden de llegada de los pedidos
JS Virtual Machine: arquitectura single threading
En una arquitectura con un solo hilo, solo podemos ejecutar un proceso a la vez.
Hilo 1
orden de llegada de los pedidos
JS Virtual Machine: arquitectura single threading
Si alguna de las operaciones tarda mucho, eso bloquea el thread: no se puede ejecutar ningún otro pedido.
Hilo 1
operación larga
va a tardar mucho en ejecutarse
JS Virtual Machine: arquitectura single threading
Incluso si hay un loop infinito, será necesario bajar y volver a subir nuestro servidor.
Hilo 1
while (true) ...
nunca llega a ejecutarse
JS Virtual Machine: arquitectura single threading
La estrategia en estos casos es hacer el procesamiento por partes: en algún momento cada función libera el control del recurso compartido (el thread) para que continue la ejecución del siguiente proceso.
Hilo 1
Cada corte es una pausa en la ejecución de la función
Las funciones son asincrónicas, pausables, también llamadas corrutinas.
JS Virtual Machine: arquitectura single threading
Aun así, las operaciones que tardan mucho (o infinito) podrían bloquear el único thread
Hilo 1
while true...
nunca se llega a ejecutar - starvation
JS Virtual Machine: arquitectura single threading
Por eso las operaciones que pueden tardar mucho (más claras) se mandan a ejecutar en otro thread.
Hilo 1
PROCESO 1
PROCESO 2
PROCESO 3
Thread pool - ejecución asincrónica
JS Virtual Machine: arquitectura single threading
Por eso las operaciones que pueden tardar mucho (más claras) se mandan a ejecutar en otro thread.
Hilo 1
PROCESO 1
PROCESO 2
PROCESO 3
Thread pool - ejecución asincrónica
PROCESO 1
PROCESO 1
JS Virtual Machine: arquitectura single threading
Por eso las operaciones que pueden tardar mucho (más claras) se mandan a ejecutar en otro thread.
Hilo 1
PROCESO 1
PROCESO 2
PROCESO 3
Thread pool - ejecución asincrónica
PROCESO 1
PROCESO 3
PROCESO 1
PROCESO 3
JS Virtual Machine: arquitectura single threading
Por eso las operaciones que pueden tardar mucho (más claras) se mandan a ejecutar en otro thread.
Hilo 1
PROCESO 1
PROCESO 2
PROCESO 3
Thread pool - ejecución asincrónica
PROCESO 2
PROCESO 3
PROCESO 1
PROCESO 2
PROCESO 3
JS Virtual Machine: arquitectura single threading
Por eso las operaciones que pueden tardar mucho (más claras) se mandan a ejecutar en otro thread.
Hilo 1
PROCESO 1
PROCESO 2
PROCESO 3
Thread pool - ejecución asincrónica
PROCESO 2
PROCESO 3
PROCESO 1
PROCESO 2
PROCESO 3
JS Virtual Machine: arquitectura single threading
Por eso las operaciones que pueden tardar mucho (más claras) se mandan a ejecutar en otro thread.
Hilo 1
PROCESO 1
PROCESO 2
PROCESO 3
Thread pool - ejecución asincrónica
PROCESO 1
PROCESO 2
PROCESO 3
¿Cómo pausamos la ejecución de un proceso?
Vamos a conocer diferentes mecanismos para construir corrutinas.
Iteradores
Definen la manera de recorrer una estructura.
Iteradores
Son muy comunes los iteradores de colecciones:
Iteradores
Pero también podemos tener un iterador para una partida de ajedrez.
const crearPartida = () => {
let blancas = true
return {
next: () => {
const result = {
value: (blancas ? "blancas" : "negras"),
done: false
}
blancas = !blancas
return result
}
}
}
Generadores
Son funciones pausables, o corrutinas, permiten ejecutar una función por partes.
instrucción 1
instrucción 2
...
instrucción n
Funciones sincrónicas
Generadores
instrucción 1
instrucción 2
instrucción 3
instrucción n
yield
yield
libera el control
y cuando vuelve (resume)
continúa en el punto anterior
Generadores
O bien...
Vamos con el ejemplo de las frutas.
function* frutasComoLista() {
yield* ['pera', 'banana', 'manzana', 'damasco']
}
function* frutas() {
yield 'pera'
yield 'banana'
console.log('ya vengo pipon')
yield 'manzana'
yield 'damasco'
}
Ejemplo: proceso que ejecuta tareas
Dado que el ejemplo es didáctico, solamente vamos a simular operaciones en las tareas leerTwitter, estudiarPromises y subirFotos.
function* estudiarPromises(): Generator<void> {
console.log('[estudiarPromises] voy a estudiar promises')
console.log('[estudiarPromises] sí que lo voy a hacer')
yield
console.log('[estudiarPromises] leo iteradores')
...
function* leerTwitter(): Generator<void> {
console.log('[leerTwitter] leemos nuestra página de Twitter]')
yield
console.log('[leerTwitter] leemos trending topics')
console.log('[leerTwitter] posteamos indignación total!!')
yield
Ejemplo: proceso que ejecuta tareas
El proceso central hace un ciclo completo, ejecutando las funciones hasta que decidan pausarse mediante el comando yield. Cuando la función termina (mediante el flag done) la sacamos de la lista de tareas
function ejecutar(tareas: Generator<void>[]) {
let i = 0
while (!isEmpty(tareas)) {
const actual = tareas[i]
const { done } = actual.next()
if (done) { tareas.splice(i, 1) } // eliminamos la tarea
i++
if (i >= tareas.length) { i = 0 }
}
}
Ejemplo: proceso que ejecuta tareas
Ejecutamos el proceso central en la consola:
Nuestro main le pide a la función principal ejecutar el proceso con estas tareas:
Y vemos cómo se alternan los logs en la consola
ejecutar([estudiarPromises(), leerTwitter()])
npx ts-node tareas.ts
Hilo principal
Event loop
Partiendo cada etapa en funciones
En lugar de tener la instrucción yield dentro de la función, vamos a partir leerTwitter en varias partes:
function* leerTwitter(): Generator<void> {
console.log(' [leerTwitter] leemos nuestra página de Twitter]')
yield
console.log(' [leerTwitter] leemos trending topics')
console.log(' [leerTwitter] posteamos indignación total!!')
yield
console.log(' [leerTwitter] mensaje privado a un amigue')
yield
console.log(' [leerTwitter] cargamos foto en la página de Twitter')
console.log(' [leerTwitter] posteamos un fotoshop gracioso')
}
Partiendo cada etapa en funciones
En lugar de tener la instrucción yield dentro de la función, vamos a partir leerTwitter en varias partes:
function* leerTwitter(): Generator<void> {
console.log(' [leerTwitter] leemos nuestra página de Twitter]')
yield
console.log(' [leerTwitter] leemos trending topics')
console.log(' [leerTwitter] posteamos indignación total!!')
yield
console.log(' [leerTwitter] mensaje privado a un amigue')
yield
console.log(' [leerTwitter] cargamos foto en la página de Twitter')
console.log(' [leerTwitter] posteamos un fotoshop gracioso')
}
function* leerPaginaTwitter(): Generator<void> {
console.log('leemos nuestra página de Twitter')
}
function* reaccionar(): Generator<void> {
console.log(' [leerTwitter] leemos trending topics')
console.log(' [leerTwitter] posteamos indignación total!!')
}
...
function* leerTwitter(): Generator<void> {
yield* leerPaginaTwitter()
yield* reaccionar()
...
console.log(' [leerTwitter] cargamos foto...')
}
Del Generator a la Promise
Si cambiamos el generador function* por async function y Generator<void> por Promise<void>:
function* leerPaginaTwitter(): Generator<void> {
console.log('leemos nuestra página de Twitter')
}
async function leerPaginaTwitter(): Promise<void> {
console.log('leemos nuestra página de Twitter')
}
Del Generator a la Promise
El proceso central es simplemente delegar a la función Promise.all que ya existe:
function ejecutar(tareas: Generator<void>[]) {
let i = 0
while (!isEmpty(tareas)) {
...
async function ejecutar(tareas: Promise<void>[]) {
await Promise.all(tareas)
}
¡Gracias!