Saltar al contenido
K
Corrutinas y FlowPaso a paso40 min Intermedio

launch y async

Al terminar: Lanzar trabajo concurrente con launch y esperar resultados con async y await.

Al terminar esta lección, Marcador jugará dos partidos a la vez en torno a 1 s (un partido solo ya dura eso), y no en más de 2 s, y calculará las estadísticas de un partido con tres consultas simultáneas. Sabrás cuándo usar launch, cuándo async, y por qué «a la vez» en runBlocking no significa lo que parece.

Es lo que hace cualquier pantalla que necesita varios datos independientes: pedirlos todos juntos en vez de uno detrás de otro. Temas: launch frente a async/await, concurrencia frente a paralelismo.

Dos formas de lanzar una corrutina

Las dos arrancan una corrutina nueva dentro de un alcance (scope: el lugar donde viven las corrutinas lanzadas; launch y async no existen fuera de uno, y en Alcance, Job y cancelación lo verás a fondo). La diferencia entre las dos es qué te devuelven:

launchasync
DevuelveJob (un control, sin resultado)Deferred<T> (una promesa de un resultado)
Para recoger el resultadono hayawait()
Úsalo cuandosolo quieres que ocurra: imprimir, guardarnecesitas el valor que calcula

Un Job es el manejador de una corrutina: sirve para esperarla (join) o cancelarla (lo verás en Alcance, Job y cancelación). Un Deferred es un Job que además guarda un valor por llegar: una promesa de un resultado que llegará.

El bloque de runBlocking es un alcance: por eso en la lección anterior pudiste lanzar launch directamente dentro, y runBlocking no termina hasta que acaban todas las corrutinas que lanzaste. Sin un alcance no hay dónde lanzar: launch y async no se pueden llamar sueltos.

ejemplo mínimo: launch y async (con comentarios)
import kotlinx.coroutines.*   // el asterisco importa todos los nombres de ese paquete de una vez
 
fun main() = runBlocking {     // runBlocking: el puente desde main, que ya conoces (y un alcance)
    // el bloque de runBlocking ya es un alcance, y runBlocking espera a todo lo que lanzas dentro
    // launch: lanza y olvida. Devuelve un Job.
    launch { delay(100); println("recado hecho") }
 
    // async: devuelve un Deferred<Int>. El valor es lo último del bloque.
    val compra = async { delay(100); 42 }
    println("mientras tanto, sigo con lo mío")   // no espera a ninguna de las dos
    println("compra: ${compra.await()}")         // await: aquí espera, si hace falta
}

Al ejecutarlo, imprime:

mientras tanto, sigo con lo mío
recado hecho
compra: 42

mientras tanto, sigo con lo mío sale enseguida. Las dos esperas acaban casi a la vez y se reanudan en el orden en que se lanzaron: por eso recado hecho sale antes que compra: 42.

Dentro de una función suspend no hay runBlocking que haga de alcance. Ahí se usa coroutineScope { }, que crea uno, no termina hasta que terminan todas las corrutinas que se lanzaron dentro y, como async, devuelve lo último de su bloque: val suma = coroutineScope { … } guarda ese valor. Lo verás en cuanto lo necesites, en los ejemplos de abajo.

«A la vez» no es «en paralelo»

Dos ideas que se confunden:

  • Concurrencia: varias tareas en curso al mismo tiempo, aunque solo una avance en cada instante. Se turnan.
  • Paralelismo: varias tareas ejecutándose en el mismo instante, en núcleos distintos. Un núcleo (se definió en Corrutinas y suspend) ejecuta un hilo en cada instante.

runBlocking usa un solo hilo. Los dos partidos de esta lección se turnan en él: cuando uno llega a un delay, el hilo pasa al otro. Es concurrencia: las dos corren en el mismo hilo.

Prueba (versión con async, misma Mac)Resultado
Dos partidos con dos async y await al final1167 ms
Los mismos, con await justo después de cada async2366 ms

Las cifras cambian de una ejecución a otra y dependen de tu máquina: cualquier valor cerca de 1-1,2 s cuenta como «a la vez» (los 90 minutos de 10 ms son 900 ms, y cada pausa añade un poco), y cerca de 2,3 s, como «en fila». La demo de Main de más abajo mide la versión con launch y da otra cifra parecida. Que las cifras no coincidan entre un método y otro no importa: importa que el total sea el de un partido y no la suma de dos.

Para tareas que esperan (red, disco, delay), concurrencia basta: el hilo no hace nada mientras espera, así que otra corrutina lo aprovecha. Para tareas que calculan, el paralelismo real exige más hilos, y de eso trata Dispatchers y withContext.

Error típico: await justo después de async

Quieres jugar dos partidos a la vez, y escribes lo natural: lanzar y recoger.

con el bug: await inmediato
suspend fun jugarJornadaLenta(primero: Partido, segundo: Partido): Pair<Partido, Partido> =
    coroutineScope {
        val a = async { jugarPartido(primero) }.await()   // espera aquí, hasta el final
        val b = async { jugarPartido(segundo) }.await()   // y solo entonces arranca este
        a to b
    }

Compila, devuelve el resultado correcto y no avisa de nada. Pero el segundo async no arranca hasta que el primero ha terminado: 2366 ms en lugar de 1167. (await no bloquea el hilo: suspende la corrutina. Aun así, lo que viene después queda esperando.)

corregido: lanzar los dos antes de esperar
suspend fun jugarJornadaRapida(primero: Partido, segundo: Partido): Pair<Partido, Partido> =
    coroutineScope {
        val a = async { jugarPartido(primero) }   // Deferred, sin esperar
        val b = async { jugarPartido(segundo) }   // arranca ya
        a.await() to b.await()                    // ahora sí, recoger
    }

Regla: todos los async primero, todos los await después.

Swift resuelve lo mismo con async let y con Task { }: lo verás en async let: varias tareas a la vez.

Un ejemplo con precios y tiendas

Todo junto en un ejemplo con precios y tiendas, explicado línea a línea (antes del ejercicio y del paso, que lo usan):

ejemplo mínimo: async con Pair y launch en un bucle (con comentarios)
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.system.measureTimeMillis
 
suspend fun precio(producto: String): Int {
    delay(100)                               // simula una consulta lenta
    return producto.length * 10
}
 
// Devuelve un Pair: dos valores juntos. Los dos async arrancan ANTES del primer await
suspend fun dosPrecios(a: String, b: String): Pair<Int, Int> = coroutineScope {
    val primero = async { precio(a) }        // Deferred<Int>: la promesa de un Int
    val segundo = async { precio(b) }
    primero.await() to segundo.await()       // a to b crea el Pair(a, b)
}
 
fun main() = runBlocking {
    val (pan, leche) = dosPrecios("pan", "leche")   // reparte el Pair en dos variables
    println("pan $pan, leche $leche")
 
    val ms = measureTimeMillis {                    // devuelve cuántos ms tardó el bloque
        coroutineScope {
            for (tienda in listOf("Norte", "Sur")) {    // una corrutina distinta por elemento
                launch { delay(100); println("Abierta: $tienda") }
            }
        }
    }
    println("Todas abiertas")                       // coroutineScope esperó a los dos launch
    println("A la vez, menos de 200 ms: ${ms < 200}")   // 100 ms, no 100 + 100
}

Al ejecutarlo, imprime:

pan 30, leche 50
Abierta: Norte
Abierta: Sur
Todas abiertas
A la vez, menos de 200 ms: true

Reto · Ejercicio: una consulta lenta entre dos rápidas

En el Kotlin Playground, con lo del ejemplo de arriba, simula tres consultas que tardan 200, 500 y 300 ms y devuelven 1, 2 y 3. Lánzalas a la vez, suma los resultados y comprueba que el total tarda menos de 700 ms; después recoge cada resultado justo después de lanzarlo y comprueba que tarda 1000 ms o más. La consola debe mostrar:

Suma: 6; a la vez, menos de 700 ms: true
Suma: 6; en fila, 1000 ms o más: true
Ver solución
solución del reto
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
 
suspend fun uno(): Int { delay(200); return 1 }
suspend fun dos(): Int { delay(500); return 2 }
suspend fun tres(): Int { delay(300); return 3 }
 
fun main() = runBlocking {
    var suma = 0
    val juntas = measureTimeMillis {
        suma = coroutineScope {
            val a = async { uno() }
            val b = async { dos() }
            val c = async { tres() }
            a.await() + b.await() + c.await()
        }
    }
    println("Suma: $suma; a la vez, menos de 700 ms: ${juntas < 700}")   // ~500 ms: lo que dura la más lenta
 
    val enFila = measureTimeMillis {
        suma = coroutineScope {
            async { uno() }.await() + async { dos() }.await() + async { tres() }.await()
        }
    }
    println("Suma: $suma; en fila, 1000 ms o más: ${enFila >= 1000}")    // ~1000 ms: la suma
}

A la vez, el total es el de la más lenta (≈ 500 ms). Con await inmediato es la suma (≈ 1000 ms).

Paso · Dos partidos a la vez y estadísticas

Objetivo. Añadir jugarJornada (dos partidos con async), Estadisticas y estadisticasDe (tres consultas simultáneas), enseñarlo desde Main.kt, y probarlo. Al terminar, la salida incluye los dos pitidos finales y dos tiempos.

torneo → kotlin+java → com.sebastiannarvaez.marcador.torneo
Simulacion.ktse modifica
Main.ktse modifica
torneo → kotlin+java → com.sebastiannarvaez.marcador.torneo (test)
JornadaTest.ktse rellena con el botón
Tres ficheros: dos se modifican y el test se rellena con el botón.

En esta lección no se instala nada: launch, async y coroutineScope son de la librería que instalaste en Corrutinas y suspend. JornadaTest.kt se rellena con un botón (más abajo) y los otros dos ficheros ya existen, en torneo → kotlin+java → com.sebastiannarvaez.marcador.torneo.

El paso no trae piezas nuevas: usa async, await y coroutineScope de arriba, launch en un bucle de El partido, minuto a minuto y measureTimeMillis de Corrutinas y suspend. Pair, a to b, val (uno, otro) = par y count { … } ya los viste en Colecciones y data class e igualdad: aquí solo los usas con async.

Intenta escribirlo tú.

  • Simulacion.kt. jugarJornada(primero, segundo, msPorMinuto) juega dos partidos a la vez y devuelve los dos resultados juntos, en una pareja; tarda lo que el más largo, no la suma. Estadisticas guarda cuántos goles, tarjetas y cambios hubo. estadisticasDe(partido, msPorConsulta) hace tres consultas a la vez (goles, tarjetas y cambios), cada una con una espera de msPorConsulta (50 ms si no se dice) que simula una base de datos, y tarda lo que una sola.
  • Main.kt, dentro de lo que ya tenías: juega el Rayo FC-Toros y el Lobos-Águilas a la vez, cada uno anunciando su pitido final en cuanto acaba, y mide el total; después juega la misma jornada con jugarJornada y mide; por último, pide las estadísticas de los dos partidos y mide.

Además de lo de la lección anterior, debe verse (los milisegundos cambian en cada ejecución; la jornada ronda 1 s y las estadísticas, 100 ms):

== dos partidos a la vez con launch ==
Pitido final: Rayo FC 2-1 Toros
Pitido final: Lobos 1-2 Águilas
Los dos partidos duraron 1117 ms (uno solo dura algo más de 1 s)
== jornada con async ==
Resultados: 2-1 y 1-2
La jornada con async duró 1167 ms
== estadísticas con async ==
Estadisticas(goles=3, tarjetas=1, cambios=1)
Estadisticas(goles=3, tarjetas=1, cambios=0)
Dos estadísticas en 105 ms (cada una son 3 consultas de 50 ms a la vez)

JornadaTest.kt no lo escribes: usa runBlocking de la misma manera que SimulacionTest, que ya escribiste tú, y lo que enseña es el código de arriba. Es un fichero de relleno, así que va con el botón Copiar fichero, como en Clases y objetos. El orden: clic derecho sobre el paquete com.sebastiannarvaez.marcador.torneo (test) → New → Kotlin Class/File → File → JornadaTest; pulsa el botón; haz clic dentro del fichero, selecciona todo con Cmd+A y pega con Cmd+V.

JornadaTest.kt

Trae 4 pruebas: dos comprueban resultados y dos comprueban que el tiempo total es el de una sola tarea, no la suma. Esta es la que enseña algo:

el test que mide «a la vez» (fragmento de JornadaTest)
@Test
fun losPartidosJueganALaVezYNoUnoDetrasDeOtro() = runBlocking {
    val ms = measureTimeMillis {
        jugarJornada(Ejemplo.rayoContraToros, Ejemplo.lobosContraAguilas, msPorMinuto = 10)
    }
    // Uno solo dura algo más de 1 s; uno detrás de otro serían más de 2 s.
    assertTrue(ms < 1500, "Tardó $ms ms: parece que no juegan a la vez")
}

Un aviso que puede salir en Main.kt: Android Studio subraya la val final que declares dentro de launch con «Name shadowed: final», porque tapa la val final de la lección anterior, que sigue en el mismo bloque. Es un aviso, no un error: el programa funciona igual.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código

En Simulacion.kt, las importaciones quedan así (delay ya estaba; async y coroutineScope son nuevas):

Simulacion.kt: los import
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay

Y al final del fichero (el resto no cambia):

Simulacion.kt
// `async` lanza una corrutina que DEVUELVE un valor: un `Deferred<T>` (una
// promesa). `await()` espera ese valor. `launch` (en Main.kt) es «lanza y
// olvida»: devuelve un `Job` sin resultado. Regla: si necesitas el resultado,
// `async`; si solo quieres que ocurra, `launch`.
//
// Dos partidos a la vez: los dos `async` arrancan antes del primer `await`, así
// que se turnan en el hilo. El total dura lo que el más largo, no la suma.
suspend fun jugarJornada(primero: Partido, segundo: Partido, msPorMinuto: Long = 10): Pair<Partido, Partido> =
    coroutineScope {
        val a = async { jugarPartido(primero, msPorMinuto) }
        val b = async { jugarPartido(segundo, msPorMinuto) }
        a.await() to b.await()
    }
 
data class Estadisticas(val goles: Int, val tarjetas: Int, val cambios: Int)
 
// Tres consultas independientes (aquí simuladas con una espera de `msPorConsulta`,
// como si fueran a una base de datos). Con `async` esperan a la vez y el total es
// ~1 consulta; una detrás de otra serían ~3.
suspend fun estadisticasDe(partido: Partido, msPorConsulta: Long = 50): Estadisticas = coroutineScope {
    val goles = async { delay(msPorConsulta); partido.eventos.count { it is Gol } }
    val tarjetas = async { delay(msPorConsulta); partido.eventos.count { it is Tarjeta } }
    val cambios = async { delay(msPorConsulta); partido.eventos.count { it is Cambio } }
    Estadisticas(goles.await(), tarjetas.await(), cambios.await())
}

En Main.kt, tres import nuevos, junto a los que ya hay (puedes escribirlos o dejar que Android Studio los añada: aviso rojo → Option+Enter):

Main.kt: import que se añaden
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlin.system.measureTimeMillis

Y dentro del runBlocking, justo después de la línea println("Final: …") de la lección anterior y antes de la llave que cierra el runBlocking (el resto del fichero no cambia):

Main.kt: las líneas nuevas
        println("== dos partidos a la vez con launch ==")
        val tiempo = measureTimeMillis {
            // Cada `launch` arranca una corrutina hija y sigue sin esperarla.
            // `coroutineScope` espera a que terminen las dos antes de continuar.
            coroutineScope {
                for (guion in listOf(Ejemplo.rayoContraToros, Ejemplo.lobosContraAguilas)) {
                    launch {
                        val final = jugarPartido(guion)
                        println("Pitido final: ${final.local.nombre} ${final.golesLocal}-${final.golesVisitante} ${final.visitante.nombre}")
                    }
                }
            }
        }
        println("Los dos partidos duraron $tiempo ms (uno solo dura algo más de 1 s)")
 
        println("== jornada con async ==")
        val tiempoJornada = measureTimeMillis {
            // `jugarJornada` devuelve un `Pair`: se reparte en dos variables.
            val (uno, otro) = jugarJornada(Ejemplo.rayoContraToros, Ejemplo.lobosContraAguilas)
            println("Resultados: ${uno.golesLocal}-${uno.golesVisitante} y ${otro.golesLocal}-${otro.golesVisitante}")
        }
        println("La jornada con async duró $tiempoJornada ms")
 
        println("== estadísticas con async ==")
        val tiempoEstadisticas = measureTimeMillis {
            println(estadisticasDe(Ejemplo.rayoContraToros))
            println(estadisticasDe(Ejemplo.lobosContraAguilas))
        }
        println("Dos estadísticas en $tiempoEstadisticas ms (cada una son 3 consultas de 50 ms a la vez)")

Punto de control

  • En la Terminal de Android Studio, ./gradlew :torneo:run termina en BUILD SUCCESSFUL e imprime, además de lo de la lección anterior:
== dos partidos a la vez con launch ==
Pitido final: Rayo FC 2-1 Toros
Pitido final: Lobos 1-2 Águilas
Los dos partidos duraron 1117 ms (uno solo dura algo más de 1 s)
== jornada con async ==
Resultados: 2-1 y 1-2
La jornada con async duró 1167 ms
== estadísticas con async ==
Estadisticas(goles=3, tarjetas=1, cambios=1)
Estadisticas(goles=3, tarjetas=1, cambios=0)
Dos estadísticas en 105 ms (cada una son 3 consultas de 50 ms a la vez)
  • Tus tiempos serán parecidos, no idénticos: cambian en cada ejecución y dependen de la máquina. Lo que debe cumplirse: cada partido o jornada ronda 1-1,2 s (no más de 2 s) y las dos estadísticas unos 100 ms en total (no 300).
  • ./gradlew :torneo:test pasa 89 pruebas (las 85 de antes más las 4 de JornadaTest), y en el paquete (test) está JornadaTest.kt.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Qué devuelve async y qué devuelve launch?

  2. 2.Escribes val a = async { jugarPartido(p1) }.await() y después val b = async { jugarPartido(p2) }.await(). ¿Qué ocurre?

  3. 3.Dentro de runBlocking, dos partidos con delay tardan lo que uno solo. ¿Qué describe mejor lo que pasa?

  4. 4.¿Qué garantiza coroutineScope { } sobre las corrutinas lanzadas dentro?

  5. 5.Necesitas mostrar, de un partido, los goles, las tarjetas y los cambios, que salen de tres consultas independientes. ¿Qué eliges?

Faltan 5 de 5