Kotlin
Tutorial Kotlin: Asincronía con suspend, async y await
Aprende a usar suspend, async y await en Kotlin para implementar corutinas asincrónicas eficientes. Mejora el rendimiento y la concurrencia en tus aplicaciones con asincronía.
Aprende Kotlin GRATIS y certifícateFunciones suspendidas suspend
Las funciones suspendidas son un elemento esencial en la programación con corutinas en Kotlin. Se declaran anteponiendo la palabra clave suspend
a la definición de la función. Esto permite que la función pueda ser suspendida y resumida sin bloquear el hilo en el que se ejecuta.
Una función suspendida puede realizar operaciones de larga duración sin afectar al rendimiento de la aplicación. Por ejemplo, al realizar una llamada a una API remota:
suspend fun obtenerDatosDelServidor(): Datos {
// Supongamos que esta función realiza una petición de red
val respuesta = api.getDatos()
return respuesta
}
Al usar suspend
, indicamos que obtenerDatosDelServidor
puede suspender su ejecución hasta que la operación de red se complete. Mientras tanto, otros procesos pueden ejecutarse en el mismo hilo.
Es importante destacar que las funciones suspendidas solo pueden ser llamadas desde otras funciones suspendidas o desde una corutina. Esto garantiza que la suspensión y reanudación de la ejecución se maneje correctamente en el contexto adecuado.
fun main() {
runBlocking {
val datos = obtenerDatosDelServidor()
println(datos)
}
}
En este ejemplo, runBlocking
inicia una corutina que puede invocar a obtenerDatosDelServidor
. La función main
espera hasta que la corutina se complete.
Las funciones suspendidas mejoran la legibilidad del código asíncrono, ya que permiten usar estructuras normales de control de flujo. No es necesario anidar callbacks o promesas, lo que facilita la gestión de errores y el mantenimiento del código.
Al combinar funciones suspendidas con otras herramientas de Kotlin, como withContext
, es posible cambiar el contexto de ejecución para realizar operaciones en hilos específicos:
suspend fun leerArchivo(): String = withContext(Dispatchers.IO) {
File("data.txt").readText()
}
Aquí, withContext
cambia al dispatcher de entrada/salida para leer un archivo sin bloquear el hilo principal.
Además, las funciones suspendidas pueden integrarse con otras bibliotecas y frameworks que soportan corutinas, permitiendo escribir código concurrente y asíncrono de manera fluida y coherente.
El uso adecuado de las funciones suspendidas contribuye a crear aplicaciones más eficientes y responsivas, especialmente en escenarios donde es crucial no bloquear el hilo de interfaz de usuario o gestionar múltiples operaciones simultáneamente.
Uso de async
El operador async
en Kotlin es una herramienta fundamental para iniciar corutinas que devuelven un resultado futuro. Cuando se utiliza async
, se inicia una corutina de forma asíncrona que devuelve una instancia de Deferred
, permitiendo realizar tareas en paralelo sin bloquear el hilo actual.
Por ejemplo, al realizar múltiples llamadas a servicios externos que pueden ejecutarse simultáneamente, async
permite lanzar estas operaciones en corutinas separadas:
import kotlinx.coroutines.*
fun main() = runBlocking {
val inicio = System.currentTimeMillis()
val resultado1 = async { operacionLenta1() }
val resultado2 = async { operacionLenta2() }
// Aquí esperamos a que ambas operaciones se completen
val suma = resultado1.await() + resultado2.await()
println("Resultado: $suma")
println("Tiempo transcurrido: ${System.currentTimeMillis() - inicio} ms")
}
suspend fun operacionLenta1(): Int {
delay(1000)
return 20
}
suspend fun operacionLenta2(): Int {
delay(1000)
return 22
}
En este ejemplo, las funciones operacionLenta1
y operacionLenta2
son funciones suspendidas que simulan una operación que tarda 1 segundo en completarse. Al utilizar async
para ambas operaciones, podemos iniciar las dos corutinas en paralelo, reduciendo el tiempo total de ejecución aproximadamente a 1 segundo en lugar de 2.
Es importante destacar que async
devuelve un objeto Deferred<T>
, donde T
es el tipo de resultado de la corutina. El método await()
se utiliza para obtener el resultado una vez que la corutina ha completado su ejecución. Si se llama a await()
y la corutina aún no ha terminado, la corutina que llama se suspenderá hasta que el resultado esté disponible.
Otro aspecto relevante es el contexto de ejecución en el que se inician las corutinas con async
. Por defecto, async
heredará el contexto de la corutina padre, pero es posible especificar un CoroutineDispatcher
diferente si es necesario:
val resultado = async(Dispatchers.IO) { tareaIntensivaEnIO() }
En este caso, la corutina se ejecutará utilizando el dispatcher IO
, optimizado para operaciones de entrada/salida.
Es crucial manejar las excepciones que puedan ocurrir dentro de las corutinas iniciadas con async
. Si una excepción no se controla adecuadamente, puede propagarse y causar comportamientos inesperados. Utilizar bloques try-catch
dentro de la corutina o manejar las excepciones al llamar a await()
es una buena práctica:
val resultado = async {
try {
operacionRiesgosa()
} catch (e: Exception) {
// Manejo de la excepción
0
}
}
También es posible utilizar async
dentro de funciones suspendidas para estructurar código asíncrono de manera más legible. Al combinar async
con otras construcciones de corutinas, se pueden crear operaciones complejas y eficientes.
Sin embargo, debe tenerse cuidado con el uso excesivo de async
, ya que puede llevar a crear demasiadas corutinas y saturar el sistema. Es recomendable limitar el número de corutinas concurrentes y utilizar estructuras como CoroutineScope
para gestionar su alcance y vida útil.
Operador await
El operador await
es esencial en Kotlin para obtener el resultado de una corutina iniciada con async
. Cuando utilizamos async
, obtenemos un objeto Deferred<T>
, que representa un valor futuro que será disponible en algún momento. El método await()
nos permite suspender la ejecución de la corutina actual hasta que el valor esté listo, sin bloquear el hilo subyacente.
Por ejemplo, consideremos una función suspendida que simula una operación costosa:
suspend fun operacionCostosa(): Int {
delay(1000) // Simula una tarea que tarda 1 segundo
return 42
}
Podemos lanzar esta operación de manera asíncrona y obtener el resultado con await
:
import kotlinx.coroutines.*
fun main() = runBlocking {
val resultadoDeferred = async { operacionCostosa() }
println("Operación iniciada...")
val resultado = resultadoDeferred.await()
println("El resultado es $resultado")
}
En este ejemplo, async
inicia la corutina que ejecuta operacionCostosa()
, y await()
suspende la ejecución hasta que el resultado esté disponible. Mientras la corutina está suspendida, otros trabajos pueden ejecutarse en el mismo hilo, optimizando el uso de recursos.
Es importante entender que await()
es una función suspendida, lo que significa que solo puede ser llamada desde otra función suspendida o dentro de una corutina. Al utilizar await()
, evitamos bloquear el hilo y mantenemos un flujo asíncrono y eficiente.
El operador await
es especialmente útil cuando necesitamos sincronizar múltiples operaciones asíncronas. Por ejemplo:
fun main() = runBlocking {
val inicio = System.currentTimeMillis()
val deferred1 = async { tareaAsincrona1() }
val deferred2 = async { tareaAsincrona2() }
// Esperamos a que ambas tareas finalicen
val resultado1 = deferred1.await()
val resultado2 = deferred2.await()
println("Resultado total: ${resultado1 + resultado2}")
println("Tiempo transcurrido: ${System.currentTimeMillis() - inicio} ms")
}
suspend fun tareaAsincrona1(): Int {
delay(2000)
return 10
}
suspend fun tareaAsincrona2(): Int {
delay(3000)
return 20
}
Aquí, las funciones tareaAsincrona1
y tareaAsincrona2
se ejecutan en paralelo gracias a async
, y utilizamos await()
para obtener sus resultados una vez que ambas han completado. El tiempo total será aproximadamente el de la tarea más lenta, no la suma de ambos delays.
El uso de await()
también nos permite manejar excepciones de manera efectiva. Si ocurre una excepción dentro de una corutina iniciada con async
, al llamar a await()
dicha excepción será lanzada. Por lo tanto, podemos capturarla y manejarla adecuadamente:
val resultadoDeferred = async {
// Código que puede lanzar una excepción
operacionQuePuedeFallar()
}
try {
val resultado = resultadoDeferred.await()
println("Operación exitosa: $resultado")
} catch (e: Exception) {
println("Error durante la operación: ${e.message}")
}
Es fundamental manejar las excepciones al utilizar await()
para evitar que se propaguen sin control y causen comportamientos inesperados en nuestra aplicación.
Además, cuando trabajamos con múltiples Deferred
, podemos utilizar funciones utilitarias como awaitAll()
para esperar a que todos completen:
val deferredList = listOf(
async { operacion1() },
async { operacion2() },
async { operacion3() }
)
val resultados = deferredList.awaitAll()
println("Resultados: $resultados")
La función awaitAll()
suspende la corutina hasta que todas las operaciones en la lista han finalizado, y devuelve una lista con sus resultados. Esto simplifica el código y mejora la legibilidad cuando manejamos varias corutinas.
Es relevante considerar el contexto de cancelación cuando utilizamos await()
. Si la corutina padre es cancelada, las corutinas hijas iniciadas con async
también serán canceladas, y cualquier llamada a await()
lanzará una excepción CancellationException
. Esto nos ayuda a mantener un control fino sobre el ciclo de vida de nuestras corutinas y evitar fugas de recursos.
Por último, al utilizar await()
, aseguramos que nuestra aplicación maneje las operaciones asíncronas de forma eficiente y coherente, manteniendo un código claro y fácil de mantener. La combinación de async
y await()
es una herramienta poderosa para construir aplicaciones concurrentes en Kotlin con corutinas.
Composición de funciones suspendidas
La composición de funciones suspendidas es una técnica esencial en Kotlin que permite combinar varias operaciones asíncronas de manera secuencial y estructurada. Al escribir funciones suspendidas que llaman a otras funciones suspendidas, podemos crear flujos de trabajo asíncronos que son claros y fáciles de mantener.
Por ejemplo, consideremos dos funciones suspendidas que realizan operaciones de red:
suspend fun obtenerUsuario(id: Int): Usuario {
// Simula una llamada a una API para obtener un usuario
delay(1000)
return api.obtenerUsuarioPorId(id)
}
suspend fun obtenerPostsDeUsuario(usuario: Usuario): List<Post> {
// Simula una llamada a una API para obtener los posts del usuario
delay(1000)
return api.obtenerPostsDeUsuario(usuario.id)
}
Podemos componer estas funciones en una sola función suspendida que obtiene el usuario y sus posts:
suspend fun obtenerUsuarioYPosts(id: Int): Pair<Usuario, List<Post>> {
val usuario = obtenerUsuario(id)
val posts = obtenerPostsDeUsuario(usuario)
return Pair(usuario, posts)
}
En este ejemplo, obtenerUsuarioYPosts
coordina las llamadas a las funciones suspendidas de forma secuencial. Gracias al mecanismo de suspensión de corutinas, este código no bloquea el hilo, permitiendo que otras tareas se ejecuten concurrentemente.
La secuencialidad en la composición de funciones suspendidas hace que el código asíncrono sea más legible y similar al código síncrono tradicional. Esto facilita la comprensión y el mantenimiento del flujo lógico de la aplicación.
También es posible manejar operaciones independientes en paralelo utilizando async
dentro de una función suspendida. Por ejemplo:
suspend fun obtenerDatosParalelamente(): Pair<DatosA, DatosB> = coroutineScope {
val datosADeferred = async { obtenerDatosA() }
val datosBDeferred = async { obtenerDatosB() }
val datosA = datosADeferred.await()
val datosB = datosBDeferred.await()
Pair(datosA, datosB)
}
En este caso, obtenerDatosA
y obtenerDatosB
son funciones suspendidas que pueden ejecutarse simultáneamente. Utilizamos coroutineScope
para asegurar que todas las corutinas hijas se completen antes de que obtenerDatosParalelamente
finalice.
La gestión de excepciones en funciones suspendidas compuestas es directa. Podemos utilizar bloques try-catch
para capturar y manejar errores de manera similar al código síncrono:
suspend fun procesarTransaccion(id: Int) {
try {
iniciarTransaccion()
val resultado = ejecutarOperacion(id)
confirmarTransaccion(resultado)
} catch (e: Exception) {
cancelarTransaccion()
throw e
}
}
Aquí, si ocurre una excepción en cualquier punto, la transacción se cancela adecuadamente, asegurando la consistencia del sistema.
Las funciones suspendidas también pueden retornar resultados que dependen de múltiples operaciones asíncronas. Veamos un ejemplo con uso de funciones de orden superior:
suspend fun <T, R> transformar(
entrada: T,
operacion: suspend (T) -> R
): R {
return operacion(entrada)
}
suspend fun procesarPedido(pedido: Pedido): Factura {
return transformar(pedido) { p ->
validarPedido(p)
val factura = generarFactura(p)
enviarFactura(factura)
factura
}
}
En este ejemplo, la función transformar
acepta una operación suspendida como parámetro, lo que nos permite crear patrones reutilizables para el procesamiento asíncrono.
La combinación de funciones suspendidas facilita el manejo de flujos de datos en aplicaciones complejas. Por ejemplo, podemos encadenar llamadas que transforman datos sucesivamente:
suspend fun procesarDatos(datosIniciales: Datos): Resultado {
val datosValidados = validarDatos(datosIniciales)
val datosTransformados = transformarDatos(datosValidados)
val resultado = guardarDatos(datosTransformados)
return resultado
}
Cada función suspendida realiza una tarea específica, y su composición crea un flujo de procesamiento claro y organizado.
Además, al componer funciones suspendidas, debemos ser conscientes de la cancelación y la propagación de la misma. Si una corutina es cancelada, todas las funciones suspendidas que se estén ejecutando también se verán afectadas, lo que nos permite implementar comportamientos receptivos a la cancelación:
suspend fun descargarArchivo(url: String) {
if (isActive) {
val contenido = clienteHttp.descargar(url)
guardarEnDisco(contenido)
}
}
Utilizando la propiedad isActive
, podemos verificar si la corutina ha sido cancelada y actuar en consecuencia.
La composición de funciones suspendidas también nos permite utilizar APIs que aceptan lambdas suspendidas, lo que amplía las posibilidades de diseño. Por ejemplo:
suspend fun <T> ejecutarTransaccion(bloque: suspend () -> T): T {
iniciarTransaccion()
return try {
val resultado = bloque()
confirmarTransaccion()
resultado
} catch (e: Exception) {
cancelarTransaccion()
throw e
}
}
suspend fun procesarPago(pago: Pago): Recibo {
return ejecutarTransaccion {
val recibido = registrarPago(pago)
generarRecibo(recibido)
}
}
Este enfoque promueve la abstracción y la reutilización de código al manejar patrones comunes en operaciones asíncronas.
Ejercicios de esta lección Asincronía con suspend, async y await
Evalúa tus conocimientos de esta lección Asincronía con suspend, async y await con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Clases genéricas con varianza y restricciones
Introducción a las corutinas
Uso de asincronía con suspend, async y await
Formateo de cadenas texto
Uso de monads y manejo funcional de errores
Declaración y uso de variables y constantes
Uso de la concurrencia funcional con corutinas
Operaciones en colecciones
Uso de clases y objetos en Kotlin
Evaluación Kotlin
Funciones de orden superior y expresiones lambda en Kotlin
Herencia y polimorfismo en Kotlin
Inmutabilidad y datos inmutables
Uso de funciones parciales y currificaciones
Primer programa en Kotlin
Introducción a la programación funcional
Introducción a Kotlin
Uso de operadores y expresiones
Sistema de inventario de tienda
Uso de data classes y destructuring
Composición de funciones
Uso de interfaces y clases abstractas
Simulador de conversión de monedas
Programación funcional y concurrencia
Creación y uso de listas, conjuntos y mapas
Transformación en monads y functors
Crear e invocar funciones
Uso de las estructuras de control
Todas las lecciones de Kotlin
Accede a todas las lecciones de Kotlin y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Kotlin
Introducción Y Entorno
Instalación Y Primer Programa De Kotlin
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Cadenas De Texto Y Manipulación
Sintaxis
Estructuras De Control
Sintaxis
Funciones Y Llamada De Funciones
Sintaxis
Clases Y Objetos
Programación Orientada A Objetos
Herencia Y Polimorfismo
Programación Orientada A Objetos
Interfaces Y Clases Abstractas
Programación Orientada A Objetos
Data Classes Y Destructuring
Programación Orientada A Objetos
Tipos Genéricos Y Varianza
Programación Orientada A Objetos
Listas, Conjuntos Y Mapas
Estructuras De Datos
Introducción A La Programación Funcional
Programación Funcional
Funciones De Primera Clase Y De Orden Superior
Programación Funcional
Inmutabilidad Y Datos Inmutables
Programación Funcional
Composición De Funciones
Programación Funcional
Monads Y Manejo Funcional De Errores
Programación Funcional
Operaciones Funcionales En Colecciones
Programación Funcional
Transformaciones En Monads Y Functors
Programación Funcional
Funciones Parciales Y Currificación
Programación Funcional
Introducción A Las Corutinas
Coroutines Y Asincronía
Asincronía Con Suspend, Async Y Await
Coroutines Y Asincronía
Concurrencia Funcional
Coroutines Y Asincronía
Evaluación
Evaluación
Certificados de superación de Kotlin
Supera todos los ejercicios de programación del curso de Kotlin y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender el uso de funciones suspendidas con
suspend
. - Implementar corutinas que devuelven resultados futuros con
async
. - Sincronizar la ejecución de corutinas utilizando
await
. - Manejar contextos de ejecución y excepciones en operaciones asíncronas.
- Aplicar composición de funciones suspendidas para crear flujos de trabajo asíncronos claros y eficientes.