Kotlin

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ícate

Funciones 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.

Aprende Kotlin GRATIS online

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.

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

Kotlin

Introducción Y Entorno

Instalación Y Primer Programa De Kotlin

Kotlin

Introducción Y Entorno

Tipos De Datos, Variables Y Constantes

Kotlin

Sintaxis

Operadores Y Expresiones

Kotlin

Sintaxis

Cadenas De Texto Y Manipulación

Kotlin

Sintaxis

Estructuras De Control

Kotlin

Sintaxis

Funciones Y Llamada De Funciones

Kotlin

Sintaxis

Clases Y Objetos

Kotlin

Programación Orientada A Objetos

Herencia Y Polimorfismo

Kotlin

Programación Orientada A Objetos

Interfaces Y Clases Abstractas

Kotlin

Programación Orientada A Objetos

Data Classes Y Destructuring

Kotlin

Programación Orientada A Objetos

Tipos Genéricos Y Varianza

Kotlin

Programación Orientada A Objetos

Listas, Conjuntos Y Mapas

Kotlin

Estructuras De Datos

Introducción A La Programación Funcional

Kotlin

Programación Funcional

Funciones De Primera Clase Y De Orden Superior

Kotlin

Programación Funcional

Inmutabilidad Y Datos Inmutables

Kotlin

Programación Funcional

Composición De Funciones

Kotlin

Programación Funcional

Monads Y Manejo Funcional De Errores

Kotlin

Programación Funcional

Operaciones Funcionales En Colecciones

Kotlin

Programación Funcional

Transformaciones En Monads Y Functors

Kotlin

Programación Funcional

Funciones Parciales Y Currificación

Kotlin

Programación Funcional

Introducción A Las Corutinas

Kotlin

Coroutines Y Asincronía

Asincronía Con Suspend, Async Y Await

Kotlin

Coroutines Y Asincronía

Concurrencia Funcional

Kotlin

Coroutines Y Asincronía

Evaluación

Kotlin

Evaluación

Accede GRATIS a Kotlin y certifícate

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

  1. Comprender el uso de funciones suspendidas con suspend.
  2. Implementar corutinas que devuelven resultados futuros con async.
  3. Sincronizar la ejecución de corutinas utilizando await.
  4. Manejar contextos de ejecución y excepciones en operaciones asíncronas.
  5. Aplicar composición de funciones suspendidas para crear flujos de trabajo asíncronos claros y eficientes.