1 of 37

Del yield al async/await

Diseño para todys

2 of 37

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.

3 of 37

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.

4 of 37

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

5 of 37

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

6 of 37

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

7 of 37

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

8 of 37

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

9 of 37

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

10 of 37

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

11 of 37

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

12 of 37

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

13 of 37

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

14 of 37

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

15 of 37

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

16 of 37

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.

17 of 37

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

18 of 37

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

19 of 37

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

20 of 37

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

21 of 37

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

22 of 37

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

23 of 37

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

24 of 37

¿Cómo pausamos la ejecución de un proceso?

Vamos a conocer diferentes mecanismos para construir corrutinas.

25 of 37

Iteradores

Definen la manera de recorrer una estructura.

26 of 37

Iteradores

Son muy comunes los iteradores de colecciones:

  • que sirven para conocer los elementos de una lista por índice
  • o bien para ordenados por algún criterio

27 of 37

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

}

}

}

28 of 37

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

29 of 37

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'

}

30 of 37

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

31 of 37

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 }

}

}

32 of 37

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

33 of 37

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')

}

34 of 37

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...')

}

35 of 37

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')

}

36 of 37

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)

}

37 of 37

¡Gracias!