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:
launch | async | |
|---|---|---|
| Devuelve | Job (un control, sin resultado) | Deferred<T> (una promesa de un resultado) |
| Para recoger el resultado | no hay | await() |
| Úsalo cuando | solo quieres que ocurra: imprimir, guardar | necesitas 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.
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: 42mientras 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 final | 1167 ms |
Los mismos, con await justo después de cada async | 2366 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.
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.)
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):
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: trueReto · 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: trueVer solución
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.
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.Estadisticasguarda 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 demsPorConsulta(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 conjugarJornaday 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.
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:
@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):
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delayY al final del fichero (el resto no cambia):
// `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):
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlin.system.measureTimeMillisY 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):
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:runtermina enBUILD SUCCESSFULe 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:testpasa 89 pruebas (las 85 de antes más las 4 deJornadaTest), y en el paquete(test)estáJornadaTest.kt.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.¿Qué devuelve
asyncy qué devuelvelaunch?2.Escribes
val a = async { jugarPartido(p1) }.await()y despuésval b = async { jugarPartido(p2) }.await(). ¿Qué ocurre?3.Dentro de
runBlocking, dos partidos condelaytardan lo que uno solo. ¿Qué describe mejor lo que pasa?4.¿Qué garantiza
coroutineScope { }sobre las corrutinas lanzadas dentro?5.Necesitas mostrar, de un partido, los goles, las tarjetas y los cambios, que salen de tres consultas independientes. ¿Qué eliges?