async()
El constructor de corrutinas async() sirve para lanzar corrutinas de las cuales necesitamos el resultado, pero permitiéndonos indicar cuándo queremos recuperarlo (es decir, sin suspender la corrutina actual hasta que sea necesario).
async() FUNCIONA EXACTAMENTE IGUAL QUE launch(): Android lo ejecuta asíncronamente cuando él lo disponga y su ejecución es concurrente a lo que suceda en otros hilos (Gestión de la ejecución de las corrutinas). La peculiaridad que tiene es que nos permite solicitar el resultado de la corrutina mediante su job. Al igual que launch(), async() devuelve un Job, pero este es de tipo Deferred<T>.
Deferred<T> tiene como particularidad la función de suspensión await(), que suspende la corrutina en la que ha sido llamado hasta que finaliza la ejecución de async() y obtenemos su resultado (<T>). Esto quiere decir que, para cuando llamemos a await(), si el resultado aún no ha sido calculado, la corrutina en la que se hace la llamada a await() se suspenderá hasta que finalice async(). Veamos cómo se usa esto (puedes leer sobre las funciones de suspensión en Funciones de suspensión, pero comprenderás mejor ese aprtado cuando termines de leer este).
Uso de await()
Tras lanzar un async(), como cualquier corrutina, Android decidirá cuándo ejecutará el código dentro de la lambda, y mientras tanto, la ejecución del resto del código sigue adelante. Pero llegará un momento en que necesitaremos el valor de async(). Entonces y lo solicitaremos mediante await().
Mira este ejemplo absurdo:
fun lanzarUnAsync() { viewModelScope.launch { val valor: Deferred<Int> = async(Dispatchers.Default) { println("Calculando el valor") 2 + 3 } println("Solicitando el resultado") val resultado = valor.await() // 5 println("El resultado es $resultado") // El resultado es 5 liveData.value = resultado }
Antes de la explicación, lo primero que debes saber es que async() solo se puede lanzar dentro de otra corrutina, ya que el método await() es una función de suspensión y por ende no se puede llamar si no es desde una corrutina u otra función de suspensión (ya explicaremos más sobre las funciones de suspensión, pero te será más fácil entenderlas si entiendes cómo funciona async() primero); por eso en el ejemplo async() está dentro de una corrutina launch().
Lo segundo es que el resultado de async() es un Deferred<T>, siendo T lo que haya en su última línea. Como la lambda de este async() devuelve un Int, tenemos que recogerlo en un Deferred<Int>. Y ahora sí, vamos a la explicación.
Lo que se hace en este código es lanzar una corrutina con launch(). Dentro lanzamos una corrutina con async() y luego esperamos a que el valor esté disponible llamando a await(). Como ya sabes, una corrutina se lanza y Android la ejecutará cuando sea. Es decir, no necesitamos llamar a await() para que async() se ejecute; la ejecución comenzará en algún momento. Solo tenemos que llamar a await() cuando queramos el resultado, es decir, cuando no podamos seguir adelante sin él.
Cuando llamemos a await(), la corrutina launch() se suspenderá (no se ejecutarán las líneas posteriores al await()) hasta que haya terminado el async() y tengamos el resultado. Si ya lo tenemos, nos lo devolverá; si async() aún está ejecutándose, nos lo devolverá cuando termine.
Como el momento en que Android ejecutará las corrutinas es impredecible, no podemos saber si por consola veremos esto:
Calculando el valor Solicitando el resultado El resultado es 5
O esto otro:
Solicitando el resultado Calculando el valor El resultado es 5
Es decir, no podemos saber si habrá ejecutado la corrutina tan rápido que se ejecutará antes de que se imprima "Solicitando el resultado" o si lo hará después, pero lo que sí sabemos es que "El resultado es 5" será siempre lo último que veamos, ya que la ejecución se detendrá en la línea del await() hasta que el resultado esté disponible.
¿Para qué vamos a usar async()?
Parece que con esto no hemos ganado mucho salvo complicar la ejecución de la corrutina un poco, ¿verdad? Pues no. No usaremos async() intercambiablemente con launch(); sino que usaremos async() cuando queramos calcular un resultado en otro hilo y no lo necesitemos hasta más adelante.
Mira este ejemplo en el que se recuperan dos valores a la par y se devuelve la suma de ambos por el LiveData.
fun lanzarVariosAsync() { viewModelScope.launch { val valor1: Deferred<Int> = async(Dispatchers.Default) { println("Calculando el primer valor") 2 } val valor2: Deferred<Int> = async(Dispatchers.Default) { println("Calculando el segundo valor") 3 } println("Solicitando el resultado") val resultado = valor1.await() + valor2.await() println("El resultado es $resultado") // El resultado es 5 liveData.value = resultado }
Este es uno de los posibles resultados que obtendremos:
Calculando el segundo valor Solicitando el resultado Calculando el primer valor El resultado es 5
O también podemos obtener:
Solicitando el resultado Calculando el primer valor Calculando el segundo valor El resultado es 5
Por los mismos motivos que antes, solamente tenemos la certeza de que "El resultado es 5" será lo último que veamos. Lo demás puede suceder tan rápido y tan concurrentemente que no podemos garantizar qué async() se ejecutará primero.
Lo que sabemos es que se ejecutarán en algún momento y que con toda probabilidad lo harán casi al mismo tiempo, porque los hemos lanzado uno después de otro y van en hilos secundarios que probablemnte serán distintos. Android no va a esperar a que termine uno para iniciar el otro (recuerda Gestión de la ejecución de las corrutinas).
Así pues, el código de launch() lanza una, lanza otra, y luego solicita el resultado con await(). Si para el momento en que solicitamos el resultado este no está disponible, la ejecución de launch() no continuará hasta que termine.
Como en este ejemplo todo sucede muy deprisa vamos a poner algún tiempo extra:
fun lanzarVariosAsync() { viewModelScope.launch { val valor1: Deferred<Int> = async(Dispatchers.Default) { println("Calculando el primer valor") delay(2000L) 2 } val valor2: Deferred<Int> = async(Dispatchers.Default) { println("Calculando el segundo valor") delay(2000L) 3 } println("Solicitando el resultado") val resultado = valor1.await() + valor2.await() println("El resultado es $resultado") // El resultado es 5 liveData.value = resultado }
Lo que hace cada async() ahora es esperar dos segundos para simular que se tarda ese tiempo en obtener el valor, y después lo devuelve. Puede que estés inclinado a creer que la ejecución durará 4 segundos, 2 de cada async(), pero no es así. Recuerda que con toda probabilidad (aunque no infalible) van a suceder casi al mismo tiempo porque Android los pondrá en dos hilos secundarios distintos). Los dos esperarán 2 segundos cada uno, pero al mismo tiempo, por lo que serán 2 segundos en total.
Así que el resultado que veremos será igual que el anterior, con la diferencia de que, para cuando llamemos al await(), no habrán transcurrido aún los 2 segundos, por lo que la ejecución de launch() se suspenderá (se parará en esa línea) hasta que esto pase y los await() devuelvan el 2 y el 3. Y cuando ya los tenga, se reanuda la ejecución del launch() y se manda el valor al LiveData.
Vamos a intentar hacer que se vea más claro poniendo otro retraso:
fun lanzarVariosAsync() { viewModelScope.launch { val valor1: Deferred<Int> = async(Dispatchers.Default) { println("Calculando el primer valor") delay(2000L) 2 } val valor2: Deferred<Int> = async(Dispatchers.Default) { println("Calculando el segundo valor") delay(2000L) 3 } delay(3000L) println("Solicitando el resultado") val resultado = valor1.await() + valor2.await() println("El resultado es $resultado") // El resultado es 5 liveData.value = resultado }
Ahora el resultado será diferente:
// Estas dos primeras líneas pueden variar de orden Calculando el segundo valor Calculando el primer valor // Pero esta línea no se imprimirá hasta transcurridos 3 segundos Solicitando el resultado El resultado es 5
Desde que se lanzan hasta que solicitamos el resultado van a transcurrir 3 segundos, pero las dos corrutinas ya habrán empezado a ejecutarse (Android las ejecutará con bastante rapidez), por lo que, cuando para cuando hayan transcurrido los 3 segundos que indicamos en el launch(), es más que probable que ya hayan transcurrido los 2 segundos concurrentes de cada async(), por lo que al llamar a await() la ejecución no tiene que detenerse y tenemos el resultado. ¡Ojo! Aunque el resultado lo tengamos ya, la ejecución se suspende igualmente aunque sea por muy poco tiempo porque estamos llamando a await(), y await() es una función de suspensión.
En resumen, como no tenemos necesidad de esperar a obtener valor1 para obtener valor2, lanzamos dos corrutinas. Estas comienzan a ejecutarse y, para cuando necesitemos el resultado, llamaremos a await() y la corrutina en la que estamos (launch()) se suspenderá hasta que los valores hayan sido calculados.
Comentarios
Publicar un comentario