Programación Asíncrona
Ejemplo: pedir un café
Aclaración: Cada cajero tiene una máquina de café
- El cliente 1 le pide al cajero un café.
- El cajero hace el café
- El cajero le da el café al cliente 1
- Luego viene el cliente 2 y repite la secuencia
De manera síncrona
Consideraciones
- Mientras se está haciendo el café, el cajero se queda esperando, no puede hacer nada más en ese "tiempo muerto".
- El cliente 2 no es atendido por nadie, ni siquiera se encola el pedido de café mientras el café del cliente 1 se está haciendo.
Manera síncrona con 2 vendedores de café
- Existe un cajero por cliente, entonces se resuelve, pero es más caro (?)
De manera asíncrona
- Se manejan de mejor manera los tiempos "muertos".
- Una vez se pide el café, se manda a hacer, y se pueden seguir recibiendo pedidos.
- Una vez termine el primer café, se lo da al primer cliente, y luego se manda a hacer el segundo
- Esto ahorra 1 cajero y 1 máquina de café, haciéndolo prácticamente en el mismo tiempo que con 2.
Ejecución Asíncrona
- Es la ejecución de una operación de cómputo en otra unidad de cómputo
- No se espera "activamente" a que dicha operación termine, sino que se "manda a hacer en el background"
- Se usan los recursos de manera más eficiente
¿Qué pasa si una función depende del resultado de otra?
fun coffeeBreak() {
val coffee = makeCoffee()
drink(coffee)
chatWithColleagues()
}
fun makeCoffee(): Coffee {
println("Making Coffee")
// Work for some time
return Coffee()
}
fun drink(coffee: Coffee) { ... }
Solución usando Callbacks
fun coffeeBreak() {
// MakeCoffee recibe un lambda para interpretar que no hace falta quedarse esperando a que termine
makeCoffee { coffee ->
drink(coffee)
}
chatWithColleagues()
}
fun makeCoffee(coffeeDone: (Coffee) -> Unit) {
println("Making Coffee")
// Work for some time
val coffee = Coffee()
coffeeDone(coffee)
}
fun drink(coffee: Coffee) { }
makeCoffee()
es lanzable en otro thread, tranquilamente.
De Sync hacia Async
Para transformar una función de sync
a async
(o al menos su firma), se debe:
- No devolver un valor
- Tomar como parámetro una continuación que defina qué hacer una vez devuelto el valor computado.
- Esta continuación en definitiva termina siendo un lambda
fun program(a: A): B {
// Do Something
return B()
}
Se lo transforma en CSP (Continuous Passing Style)
fun asyncProgram(a: A, c: (B) -> Unit) {
// Do Something
c(B())
}
Callback Hell
¿Qué sucede si otro programa depende de que el coffee break haya terminado?
Por ejemplo, una conferencia incluye un coffee break en el medio:
fun coffeeBreak(breakDone: ()->Unit)
fun conference() {
presentation { p1 ->
coffeeBreak {
presentation { p2 ->
endConference(p1, p2)
}
}
}
}
Esto se vuelve ilegible, en definitiva. Escala muy poco. También se lo llama "The doom pyramid"
Futures
Es análogo al Promise
de JS/TS
. Es una "promesa" o un registro de que se llamó a una función asíncrona, por así decirlo.
Este Future va a devolver un valor en algún momento (cuando quiera usar el valor).
Se propaga "para arriba" en la jerarquía de llamados el cuándo espero por el valor.
Es decir, si yo me quedo esperando por un valor asíncrono (por un Promise
), la función donde espero por dicho valor se vuelve asíncrona.
En el caso del ejemplo del café, se hace cuando quiera tomar el café, por ejemplo.
Se intenta pasar de esto:
fun program(a: A): B {}
// CSP
fun program(a: A, k: (B) -> Unit) {}
A esto:
fun program(a: A): Future<B> {}
Currying
Esencialmente, pasamos de esto:
fun program(a: A, k: (B) -> Unit) : Unit {}
- Tomamos un callback como parámetro y lo ejecutamos
A esto:
fun program(a: A): ((B) -> Unit) -> Unit {}
- Devolvemos una función que toma una función que devuelve un
Unit
, que termina devolviendo unUnit
Usando Currying (como en Haskell)
add :: Num a => a -> a -> a
add x y = x + y
-- El tipo de `add 10` va a ser:
add 10 :: Num a => a -> a
- En Haskell yo me puedo guardar una función en una variable con un parámetro con un valor "por default", e invocarla en otro lado. Es decir, "invocarla parcialmente".
- Justamente eso es Currying.
Ejemplo del Café usando Future
fun makeCoffeeFuture(): Future<String> {
return CompletableFuture.supplyAsync {
println("Making Coffee")
sleep(2000)
"coffee ${coffeeNumber.incrementAndGet()}"
}
}
// Función principal
fun futureCoffeeBreak() {
val f: Future<String> = makeCoffeeFuture() // Mando a hacer el café
chatWithColleagues() // Me pongo a charlar con mis amigos, o a hacer otra cosa
drink(f.get()) // Me tomo el café una vez listo
}
Future.get()
espera a que el valor esté listo para usar, devolviéndolo.- Nótese que
coffeeNumber
es unAtomicInteger
, justamente para mantenernos en el contexto concurrente.
De manera non-blocking con Futures
import java.util.concurrent.CompletableFuture
fun futureCoffeeBreak() {
val f: CompletableFuture<String> = makeCoffeeFuture()
f.thenAccept { coffee ->
drink(coffee)
}
chatWithColleagues()
}
Manejar errores con Futures
fun futureCoffeeBreak() {
val f: CompletableFuture<String> = makeCoffeeFuture()
f.thenAccept { coffee ->
drink(coffee)
}
.handle { r, exception ->
println("Failed with $exception")
}
chatWithColleagues()
}
Combinando Futures
fun futureCoffeeBreakBlended() {
val f1 = makeCoffeeFuture()
val f2 = makeCoffeeFuture()
val combinedFuture = f1.thenCombine(f2) { result1, result2 ->
"$result1 blended with $result2"
}
combinedFuture.thenAccept { c ->
drink(c)
}
chatWithColleagues()
}
En definitiva, los Futures
son Monads
Cabe recordar que un Monad
en Haskell
es una interfaz para wrappear valores en un contexto, y operar con los valores internos del contexto.
Recordar el tipo Maybe a = Just a | Nothing
.
blendedCoffee =
do
coffee1 <- makeCoffeeFuture
coffee2 <- makeCoffeeFuture
return coffee1 ++ " Blended With " ++ coffee2
- Esto es exactamente igual al
Future<T>.thenAccept(Future<T>)