Kotlin

Kotlin

Tutorial Kotlin: Introducción a las corutinas

Kotlin: Introducción a las corutinas para simplificar la programación concurrente y asíncrona, mejorando eficacia y claridad en tus proyectos.

Aprende Kotlin GRATIS y certifícate

¿Qué son las corutinas y qué ventajas tienen?

Las corutinas en Kotlin son una herramienta para manejar la programación asíncrona y concurrente de manera más sencilla y eficiente. Son funciones que pueden suspender y reanudar su ejecución sin bloquear el hilo en el que se ejecutan. Esto permite escribir código asíncrono de forma secuencial, lo que mejora la legibilidad y mantenibilidad del código.

A diferencia de los hilos tradicionales, las corutinas son muy ligeras y pueden gestionarse en grandes cantidades sin incurrir en un alto costo de recursos. Esto se debe a que las corutinas están soportadas por el compilador y la biblioteca estándar de Kotlin, lo que optimiza su ejecución.

Las ventajas principales que tienen las corutinas sobre los hilos tradicionales son:

  • La capacidad de evitar el callback hell (un problema común en la programación asíncrona tradicional donde las funciones anidadas dificultan la lectura del código).
  • Permiten un manejo más eficiente de las tareas concurrentes.
  • Se integran fácilmente con APIs existentes y bibliotecas de terceros.
  • Ofrecen un control preciso sobre los contextos de ejecución, lo que ayuda a evitar problemas de concurrencia y sincronización.

Creación y uso de corutinas

Antes de empezar con las corutinas primero tenemos que implementar las librerías en nuestro proyecto

Para crear y utilizar corutinas en Kotlin, es fundamental entender cómo funcionan los constructores de corutinas y las funciones suspendidas. Una corutina se inicia utilizando constructores como launch, async o runBlocking, disponibles en la biblioteca kotlinx.coroutines.

Por ejemplo, para iniciar una corutina que realice una tarea en segundo plano, se puede utilizar el constructor launch:

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        val resultado = obtenerDatos()
        println("Resultado: $resultado")
    }
}

suspend fun obtenerDatos(): String {
    delay(1000) // Simula una operación asincrónica
    return "Datos obtenidos"
}

En este código, la función main utiliza runBlocking para iniciar un Contexto de corutina. Dentro de este contexto, launch crea una nueva corutina que ejecuta la función obtenerDatos(). La palabra clave suspend en la definición de obtenerDatos() indica que es una función suspendida, capaz de suspender su ejecución sin bloquear el hilo.

Las funciones suspendidas son fundamentales en la programación con corutinas. Permiten realizar operaciones asincrónicas de forma secuencial y legible. Por ejemplo, se puede llamar a varias funciones suspendidas en orden:

suspend fun procesarInformacion() {
    val datosUsuario = obtenerDatosUsuario()
    val preferencias = obtenerPreferencias()
    mostrarInformacion(datosUsuario, preferencias)
}

Aquí, procesarInformacion() llama secuencialmente a otras funciones suspendidas, facilitando el flujo lógico del programa.

Para ejecutar corutinas de manera paralela y obtener resultados, se utiliza el constructor async junto con la función await():

suspend fun calcularResultados() = coroutineScope {
    val resultado1 = async { operacionLarga1() }
    val resultado2 = async { operacionLarga2() }
    println("Resultados: ${resultado1.await()}, ${resultado2.await()}")
}

En este ejemplo, operacionLarga1() y operacionLarga2() se ejecutan simultáneamente, y await() se utiliza para obtener sus resultados una vez completadas.

Es importante manejar correctamente las excepciones en las corutinas. Las estructuras de control como try-catch funcionan de manera natural:

launch {
    try {
        val datos = obtenerDatosSeguros()
        procesarDatos(datos)
    } catch (e: Exception) {
        println("Error al obtener datos: ${e.message}")
    }
}

La correcta gestión de excepciones asegura que los errores sean manejados sin interrumpir el flujo del programa.

Las corutinas también permiten establecer timeouts para operaciones que no deben exceder un tiempo determinado:

withTimeout(5000) {
    realizarOperacionCritica()
}

Si realizarOperacionCritica() no finaliza en 5 segundos, se lanzará una excepción TimeoutCancellationException, permitiendo manejar situaciones donde es crucial no superar cierto tiempo.

Es posible cancelar corutinas cuando ya no sean necesarias, mejorando la eficiencia del programa:

val job = launch {
    repetirTarea()
}
delay(2000)
job.cancel() // Cancela la corutina después de 2 segundos

La cancelación de corutinas es una práctica importante para evitar consumo innecesario de recursos.

Para estructuras más complejas, como operaciones que dependen unas de otras, las corutinas ofrecen soluciones elegantes. Por ejemplo, se puede utilizar async para iniciar tareas que alimenten a otras:

suspend fun procesarCadena() = coroutineScope {
    val paso1 = async { primerPaso() }
    val paso2 = async { segundoPaso(paso1.await()) }
    val resultado = paso2.await()
    println("Resultado final: $resultado")
}

Este enfoque permite componer funciones suspendidas de manera clara y mantenible.

Finalmente, es esencial comprender el uso de scope builders, como coroutineScope y supervisorScope, que controlan la propagación de excepciones y la vida de las corutinas hijas:

suspend fun realizarTareas() = supervisorScope {
    val tarea1 = launch { tareaImportante1() }
    val tarea2 = launch { tareaImportante2() }
    // Si tarea1 falla, tarea2 continúa ejecutándose
}

Utilizando supervisorScope, se garantiza que una excepción en una corutina no afecta a las demás, lo cual es crucial en ciertas aplicaciones.

Scopes y contextos de corutinas

El concepto de scope de corutina es esencial para manejar de forma organizada y segura las corutinas en Kotlin. Un scope define el ciclo de vida y el contexto en el que se ejecutan las corutinas, permitiendo controlar su inicio y cancelación de manera estructurada.

Para crear un scope de corutina, se utilizan constructores como CoroutineScope o se hereda de clases que lo implementan. Por ejemplo, en una aplicación podemos definir un scope personalizado:

class MiActividad : AppCompatActivity(), CoroutineScope {
    private val job = Job()
    override val coroutineContext = Dispatchers.Main + job
}

En este caso, combinamos el contexto de corutina con un Dispatcher y un Job. El contexto de corutina es un conjunto de elementos que definen aspectos como el hilo de ejecución (Dispatcher), el Job asociado y más información relevante para su ejecución.

El Dispatcher determina en qué hilo o hilos se ejecutará la corutina. Kotlin proporciona varios dispatchers predefinidos:

  • Dispatchers.Main: Utilizado para actualizaciones de la interfaz de usuario.
  • Dispatchers.IO: Optimizado para operaciones de entrada/salida intensivas.
  • Dispatchers.Default: Adecuado para tareas intensivas en CPU.
  • Dispatchers.Unconfined: Inicia la corutina en el hilo actual, pero puede cambiar en suspensiones.

Por ejemplo, para lanzar una corutina en el dispatcher de IO:

launch(Dispatchers.IO) {
    val datos = leerDesdeDisco()
    withContext(Dispatchers.Unconfined) {
        actualizarUI(datos)
    }
}

En este código, leemos datos desde el disco en un hilo de IO y luego actualizamos la interfaz de usuario en el hilo principal usando withContext(Dispatchers.Unconfined).

El uso de withContext es esencial cuando necesitamos cambiar el contexto dentro de una corutina sin crear una nueva. Esto es útil para optimizar el rendimiento y mantener el código limpio.

Las corutinas se componen de Jobs, que representan una unidad de trabajo. Los jobs pueden tener una jerarquía, donde la cancelación de un job padre implica la cancelación de todos sus jobs hijos. Esto es clave para implementar concurrencia estructurada, asegurando que no queden corutinas huérfanas ejecutándose sin control.

Por ejemplo, al usar CoroutineScope:

fun procesarDatos() = CoroutineScope(Dispatchers.Default).launch {
    val resultado = async { calculoComplejo() }
    println("Resultado: ${resultado.await()}")
}

Aquí, creamos un scope que se ejecuta en el dispatcher por defecto y lanzamos una corutina. Si cancelamos el scope, todas las corutinas lanzadas dentro de él serán canceladas automáticamente.

La gestión de excepciones en corutinas es otro aspecto crítico. En un scope de corutina, podemos manejar excepciones utilizando try-catch, asegurándonos de capturar errores sin detener todo el flujo:

launch {
    try {
        realizarOperacion()
    } catch (e: Exception) {
        println("Error: ${e.message}")
    }
}

Sin embargo, cuando las corutinas son lanzadas con launch, las excepciones no propagadas caen en el CoroutineExceptionHandler. Por ello, es importante establecer un handler si necesitamos gestionar excepciones no capturadas:

val handler = CoroutineExceptionHandler { _, exception ->
    println("Excepción capturada: $exception")
}

launch(handler) {
    // Código que puede lanzar una excepción
}

Con async, las excepciones se propagan cuando se llama a await(). Esto nos permite manejar errores de manera diferida:

val resultado = async {
    // Código que puede fallar
}

try {
    resultado.await()
} catch (e: Exception) {
    println("Error al obtener el resultado: ${e.message}")
}

Es posible definir un contexto personalizado añadiendo elementos al contexto existente. Por ejemplo, para añadir un identificador de logging:

val contextoPersonalizado = coroutineContext + CoroutineName("MiCorutina")
launch(contextoPersonalizado) {
    // Código de la corutina
}

El uso de nombres de corutina facilita el seguimiento y depuración, especialmente en aplicaciones complejas.

La función supervisorScope es útil cuando necesitamos que las corutinas hijas continúen ejecutándose incluso si alguna falla. En un supervisorScope, las excepciones no se propagan al job padre:

supervisorScope {
    launch {
        // Esta corutina puede fallar
    }
    launch {
        // Esta corutina continuará ejecutándose
    }
}

Esto es especialmente útil en escenarios donde queremos asegurarnos de que ciertas tareas críticas se completen independientemente de fallos en otras corutinas.

Para cancelar corutinas de manera controlada, utilizamos señales de cancelación verificando periódicamente el estado de la corutina con isActive:

suspend fun tareaCancellable() = coroutineScope {
    while (isActive) {
        // Realizar trabajo
    }
}

Así, la corutina responde a la cancelación de forma cooperativa, permitiendo liberar recursos adecuadamente.

El uso de Dispatchers y la correcta elección del contexto optimiza el rendimiento y evita bloqueos. Por ejemplo, es recomendable evitar operaciones pesadas en Dispatchers.Main y delegarlas a Dispatchers.IO o Dispatchers.Default según corresponda.

Para ejecutar corutinas sin un scope explícito, podemos usar GlobalScope, aunque se recomienda evitarlo en la mayoría de los casos debido a que las corutinas lanzadas así viven durante toda la aplicación, lo que puede causar fugas de memoria:

GlobalScope.launch {
    // Corutina de larga duración
}

Es preferible utilizar scopes vinculados al ciclo de vida de la aplicación o componente para un mejor control.

Aprende Kotlin GRATIS online

Ejercicios de esta lección Introducción a las corutinas

Evalúa tus conocimientos de esta lección Introducción a las corutinas 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 qué son las corutinas y sus ventajas frente a los hilos tradicionales.
  2. Aprender a crear y usar corutinas empleando constructores como launch y async.
  3. Manejar excepciones y aplicar timeouts en corutinas.
  4. Entender cómo administrar los scopes y contextos de ejecución.
  5. Implementar concurrencia estructurada y gestionar la cancelación de corutinas.
  6. Optimizar el uso de Dispatchers en diferentes escenarios.