Saltar a contenido

Corrutinas

Kotlin incorpora un potente sistema de programación asíncrona basado en coroutines (corrutinas). Permiten ejecutar tareas de forma no bloqueante, evitando congelar la interfaz o detener el flujo principal de la aplicación.

Son fundamentales cuando realizamos:

  • consultas a bases de datos
  • peticiones a APIs
  • descargas de archivos
  • operaciones pesadas o lentas

¿Qué son las corrutinas?

Una corrutina es un subproceso ligero, gestionado por Kotlin y no por el sistema operativo. Se pueden lanzar miles sin bloquear el hilo principal. Una corrutina es conceptualmente similar a un hilo (thread) que se implementan en otros lenguajes o inclusive en Kotlin ya que podemos acceder a la clase Thread de Java. Las corrutinas se pueden considerar como subprocesos livianos.

Para trabajar con corrutinas debemos importar la librutina:

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")

Info

Usa siempre la última versión estable disponible.

runBlocking: puente entre código normal y corrutinas

runBlocking es también un constructor de corrutinas que une el mundo que no es de corrutina de una función regular fun main() y el código con corrutinas dentro de runBlocking. Hasta que no finalizan por completo todas las corrutinas que tienen dentro de runBlocking, no finaliza la función main.

fun main() = runBlocking {
    println("Iniciando...")
    delay(1000)
    println("Finalizado.")
}

Lanzar una corrutina

La creación de una corrutina se logra llamando a la función 'launch' y pasando una función lambda con el algoritmo que queremos que se ejecute en forma paralela al hilo principal de nuestro programa. Recordemos que en Kotlin podemos indicar directamente la implementación de la función, en lugar de disponer el nombre del parámetro, la signación y la implementación de la función.

import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Inicio del programa")

    launch {
        for (i in 1..10) {
            println("Número: $i")
            delay(1000)
        }
    }

    println("Fin inmediato (la corrutina sigue en segundo plano)")
}

Lanzar dos corrutinas simultáneas

fun main() = runBlocking {
    launch {
        for (i in 1..10) {
            println("A: $i")
            delay(500)
        }
    }

    launch {
        for (i in 11..20) {
            println("B: $i")
            delay(700)
        }
    }
}

Funciones suspendidas (suspend)

Una función de suspensión solo puede ser llamada desde una corrutina o desde otra función de suspensión. Las funciones de suspensión llamadas desde una corrutina se ejecutan en forma secuencial por defecto. Se inician con la palabra suspend

suspend fun procesoLento() {
    delay(1000)
    println("Proceso terminado")
}

fun main() = runBlocking {
    procesoLento()
}

Refactorizando en función suspendida

suspend fun imprimirSecuencia() {
    for (i in 1..5) {
        println("Valor: $i")
        delay(500)
    }
}

fun main() = runBlocking {
    launch { imprimirSecuencia() }
}

launch retorna un Job

La función launch retorna un objeto de tipo Job que es un identificador de la corrutina iniciada y se puede usar para esperar explícitamente a que se complete. Dicha acción se hace llamando al método join.

fun main() = runBlocking {
    val tarea: Job = launch {
        delay(1000)
        println("Tarea completada")
    }

    tarea.join()
    println("Programa finalizado")
}

coroutineScope: alternativa no bloqueante

Además del alcance de la corrutina proporcionado por diferentes constructores, es posible declarar su propio alcance utilizando el constructor coroutineScope. Crea un alcance de corrutina y no se completa hasta que se completan todos los elementos secundarios iniciados.

Los constructores de corrutinas runBlocking y coroutineScope pueden parecer similares porque ambos esperan que su cuerpo y todos sus elementos secundarios se completen. La principal diferencia es que el método runBlocking bloquea el hilo actual para esperar, mientras que coroutineScope simplemente suspende, liberando el hilo subyacente para otros usos. Debido a esa diferencia, runBlocking es una función regular y coroutineScope es una función de suspensión.

Se puede utilizar un constructor coroutineScope dentro de cualquier función de suspensión para realizar múltiples operaciones simultáneas.

suspend fun ejemploScope() = coroutineScope {
    launch {
        delay(500)
        println("Primera tarea")
    }
    launch {
        delay(800)
        println("Segunda tarea")
    }
}

fun main() = runBlocking {
    ejemploScope()
    println("Todo terminado")
}

Llamadas concurrentes con async

En algunas situaciones si el problema lo permite podemos ejecutar las funciones de suspensión en forma concurrente y eventualmente si disponemos de varios procesadores la ejecución se puede hacer en paralelo con la ventaja de reducir el tiempo.

suspend fun tareaLenta1(): Int {
    delay(1000)
    return 10
}

suspend fun tareaLenta2(): Int {
    delay(2000)
    return 20
}

fun main() = runBlocking {
    val a = async { tareaLenta1() }
    val b = async { tareaLenta2() }

    println("Resultado: ${a.await() + b.await()}")
}

Actividades

  • AC 505 (RA2 / CE2a / IC1 / 1p). Lanza dos corrutinas usando launch.

    • Una imprime los múltiplos de 5 del 1 al 100 cada 300 ms.
    • La otra imprime los múltiplos de 2 del 100 al 200 cada 500 ms.

    Observa cómo se intercalan los resultados.

  • AC 506 (RA2 / CE2a / IC1 / 1p). Crea dos funciones suspend: descargarDatos() y procesarDatos(), cada una con un delay distinto. Ejecútalas con async para que corran simultáneamente y mide el tiempo total de ejecución.

    Simula

    El contenido de las funciones que nazca de tu imaginación.