Tabla de Contenidos
Corrutinas Kotlin
La programación asíncrona o sin bloqueo es resuelta en el lenguaje Kotlin mediante una tecnología llamada Coroutines que viene implementada una parte en el lenguaje Kotlin propiamente dicho y otra gran parte mediante una biblioteca de funciones.
La programación asíncrona es necesaria para desarrollar algoritmos que requieren muchos recursos, consultas a servidores de internet, consultas a bases de datos, descarga de archivos grandes etc. con el objetivo de no bloquear el hilo principal de nuestra aplicación y que el usuario se vea impedido de interactuar con el programa hasta que termine de ejecutar el algoritmo.
Las corrutinas permiten no bloquear el hilo principal de la aplicación.
También son indispensables para implementar aplicaciones escalables, podemos tener programas mucho más escalables ejecutando distintas rutinas en forma simultánea en distintos procesadores.

En GitHub Corrutinas (rama starter) tenéis el código starter de un proyecto Android con un Módulo Kotlin (Corrutinas) para poder ir ejecutando lo necesario. Seleccionar la Run Configuration KotlinMain para no ejecutar en el emulador, tal como se ve en la figura.
Veréis en el código que cada uno de los siguientes apartados tiene un bloque de código que puede estar comentado. Tendréis que ir des-comentando y comentando apropiadamente para poder seguir la ejecución de todo, paso a paso.
En el módulo de Corrutinas, en el fichero build.gradle añadir o modificar la dependencia para que funcionen las corrutinas en android studio.
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' }
Ejercicio 1
En el fichero MainKt cambiar la función main por la siguiente:
fun main(args: Array<String>) { //EJERCICIO 1 log("Inicio del programa"); GlobalScope.launch { log("Inicio de la corrutina") for(x in 1..10) { print("$x -") delay(1000) } } log("Se bloquea el hilo principal del programa al llamar a readLine") readLine() } fun log(message: String) { println("[${Thread.currentThread().name}] : $message") } fun log(character: Char) { print("$character") }
La opción de GlobalScope se entiende como un recurso global que no se encuentra vinculado a ningún job y genera un nuevo hilo de ejecución global.
Este componente se utiliza en Kotlin con el fin de lanzar las coroutines de nivel superior, caracterizadas por su funcionamiento durante toda la vida útil de la aplicación, lo que además implica que no son canceladas de forma prematura. (veremos que no es buena práctica lanzarlas así en Android, pero aquí estamos en Kotlin y entendiendo qué son las corrutinas)
Como podemos comprobar lo primero que aparece en pantalla es el mensaje que muestra las llamadas a log:
log("Se bloquea el hilo principal del programa al llamar a readLine")
Seguidamente bloqueamos el hilo principal de nuestro programa llamando a la función readLine (si el programa finaliza, todas las corrutinas que se hay iniciado finalizan en forma automática):
readLine()
Luego podemos comprobar que comienzan a aparecer en pantalla los números del 1 al 10 de uno en uno, lentamente.
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:
GlobalScope.launch { for(x in 1..10) { print("$x -") delay(1000) } }
Dentro de la función lambda disponemos un for que se repetirá 10 veces y en su interior mostramos el contador y detenemos la ejecución de la corrutina mediante la llamada a la función delay pasando la cantidad de milisegundos a detenerse.
Finalmente hemos puesto un readLine() para que el usuario pulse enter para terminar el hilo principal, de esta forma el hilo principal se espera mientras el usuario no pulse y así la corrutina puede evolucionar. Ya que si se termina el hilo donde se lanzó la corrutina, ésta terminará también. Es decir, si mientras se está mostrando la cuenta el usuario pulsa enter, como el hilo principal estaba esperando la entrada de usuario, la lee y termina, terminando por tanto la corrutina.
Ejercicio 2
Ahora vamos a lanzar dos corrutinas: En la primera mostrar los números del 1 al 10 y en la segunda los números del 11 al 20.
// EJERCICIO 2 GlobalScope.launch { log("Inicio de la corrutina 1") for(x in 1..10) { print("$x ") delay(1000) //sleep(1000) } } GlobalScope.launch { log("Inicio de la corrutina 2") for(x in 11..20) { print("$x ") sleep(1000) //delay(1000) } } readLine()
Notar que las dos corrutinas se ejecutan en forma simultánea, no se requiere terminar la primer corrutina donde se muestran los números del 1 al 10 para que comience la segundo corrutina donde se muestran los números del 11 al 20, como vemos aparecen intercalados los resultados de cada corrutina. Normalmente cada vez que se lance el resultado será diferente, a veces una corrutina adelanta a la otra. Cada corrutina se lanza en un thread diferente de nivel global.
En este caso podemos utilizar sleep o delay.
- Sleep es una función externa del sistema, realmente es Thread.sleep() que para el hilo de ejecución.
- Pero delay() es una función de suspensión, es una función que suspende (para) la ejecución de una corrutina, no del thread en la que corre la corrutina.
Como en este ejemplo tenemos dos corrutinas, pero que se han lanzado con Globalscope, están ejecutando en threads distintos, y por eso en este caso el resultado de ejecución es indistinto al usar delay o sleep.
Ejercicio 3
Desarrollar un programa que en el hilo principal genere un número aleatorio entre 1 y 100
Luego una corrutina intentará adivinar el número cada 500ms, obtendrá un valor entre inicio=1 y fin=100, que comparara con el valor del numero generado en el hilo principal, si es mayor o menor, mostrará un mensaje y actualiza sus variables inicio y fin. Así hasta que encuentre el número.
Analizar el código solución siguiente:
import kotlin.random.Random fun main(args: Array<String>) { //EJERCICIO 3 log("Ejecución en el hilo principal. Adivina el número entre 1 y 100 ") val numero = Random.nextInt(1, 100) var inicio = 1 var fin = 100 GlobalScope.launch { var valor:Int log("Inicio de la corrutina adivinadora") do { valor = Random.nextInt(inicio, fin) println(valor) if (valor == numero) println("En número es el $valor") else if (valor < numero) { println("El numero es mayor") inicio = valor } else { println("El numero es menor") fin = valor } delay(500) } while (valor != numero) } readLine() //detenemos el hilo principal del programa }
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 (son gestionados por la librería de corrutinas y comparten el hilo que se indique, pero no lo bloquean si se realizan suspensiones en ellas, por ejemplo Delays.
Si el hilo principal de nuestro programa finaliza luego todas las corrutinas en ejecución también finalizan.
Ahora veremos que mediante la llamada a la función runBlocking podemos bloquear nuestro hilo principal de la aplicación hasta que todas las corrutinas finalicen.
Ejercicio 4
Probar a ejecutar el siguiente código
fun main(args: Array<String>) = runBlocking { //Para probar esto hay que añadir " = runBlocking" en la función main log("Running in the main thread") launch { delay(1000) log("Paso un segundo") } log("After runBlocking") println("Fin del Main") }
La salida es:
[main] : Running in the main thread [main] : After runBlocking Fin del main [main] : Paso un segundo Process finished with exit code 0
Que también es lo mismo si hacemos:
fun main(args: Array<String>) { //Con RunBlocking //Prueba a ejecutar en el scope prinpical. Quitar el runBlocking de la función main log("Running in the main thread") runBlocking { launch { delay(1000) log("Paso un segundo") } } log("After runBlocking") println("Fin del Main") }
Hemos usado runBlocking que es otra forma de lanzar una corrutina, pero en este caso bloquea el hilo desde el que se lanza hasta que la corrutina termina. Aquí, no se crea un nuevo hilo para la corrutina, sino que se ejecuta en el hilo en el que se llama a runBlocking, en este caso [Main]. La salida es:
[main] : Running in the main thread [main] : Paso un segundo [main] : After runBlocking Fin del main Process finished with exit code 0
De esta forma nos evitamos poner el readLine() final para que el hilo principal espere la terminación de la corrutina.
Si nos fijamos en la ejecución:
En el primer código, runBlocking
se utiliza como un bloque de construcción de la función main
. Esto significa que la función main
se ejecutará en el contexto de la corrutina proporcionada por runBlocking
. En este caso, runBlocking
no bloquea el hilo principal, sino que simplemente proporciona un contexto de corrutina para la función main
. Por lo tanto, cuando se lanza la corrutina con launch
, se ejecuta de forma asíncrona con respecto al hilo principal, lo que permite que el hilo principal continúe ejecutándose. Esto es lo que permite que “Fin del Main” se imprima antes de “Paso un segundo”.
En el segundo código, runBlocking
se utiliza dentro de la función main
. En este caso, runBlocking
bloquea el hilo principal hasta que se completa la corrutina que se lanza dentro de él. Por lo tanto, “Paso un segundo” se imprime antes de que el hilo principal pueda continuar y imprimir “Fin del Main”.
runBlocking
se utiliza para bloquear el hilo actual hasta que se complete la corrutina que se lanza dentro de él. Si se utiliza como bloque de construcción de la función main
, proporciona un contexto de corrutina para la función main
pero no bloquea el hilo principal.
Hasta que no finalizan por completo todas las corrutinas que tienen dentro de runBlocking (una en este caso que llamamos mediante la función launch), no finaliza la función main.
launch es un constructor de corrutinas. Lanza una nueva corrutina al mismo tiempo que el resto del código, que continúa funcionando de forma independiente.
Refactorización de funciones.
Cuando queremos pasar un algoritmo contenido en una corrutina a una función, es decir un trozo del código de la corrutina lo queremos encapsular en una función y llamarla desde la corrutina, entonces, a la función que creemos tiene que llevar el el modificador ‘suspend‘.
Veamos los cambios que hay que hacer con el ejemplo anterior:
fun main(args: Array<String>) { //Refactorización de funciones. log("Running in the main thread") runBlocking { launch { log("En el hilo de la corrutina") espera() //Esta debera ser una función suspendida por estar dentro de una corrutina } } log("After runBlocking") } suspend fun espera() { delay(1000) //esto tambien es una función de suspension println("Pasó un segundo") }
Funciones de Suspensión
Una función de suspensión (o suspend function
) en Kotlin es una función que puede pausar su ejecución sin bloquear el hilo donde se ejecuta, y reanudarse más tarde. Es la base de las corrutinas.
Las funciones de suspensión se pueden usar dentro de las corrutinas al igual que las funciones normales, pero su característica adicional es que pueden, a su vez, usar otras funciones de suspensión (como delay en este ejemplo) para suspender la ejecución de una corrutina.
🧠 ¿Qué significa “suspenderse”?
Imagina que una función necesita esperar 2 segundos (por ejemplo, para hacer una consulta a Internet).
Si fuera una función normal, bloquearía el hilo (¡incluido el de la UI!). Pero una función de suspensión:
- Se detiene sin bloquear el hilo
- Libera recursos mientras espera
- Luego, continúa justo donde se quedó
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, por ejemplo probemos el siguiente código que llama a dos funciones de suspensión:
fun main(args: Array<String>) = runBlocking { val d1=dato1() log("Fin de la primera función de suspensión") val d2=dato2() log("Fin de la segundo función de suspensión") print(d1+d2) } suspend fun dato1(): Int { delay(3000) return 3 } suspend fun dato2(): Int { delay(3000) return 3 }
En muchas situaciones las llamadas secuenciales de las funciones de suspensión son la solución correcta, por ejemplo solicitamos a un servidor un dato y a partir de dicho dato hacemos la petición a otro servidor a partir del dato recuperado del primer servidor.
El tiempo de ejecución de las dos funciones de suspensión es aproximadamente de 6 segundos, esto debido a que se ejecutan en forma secuencial.
📲 ¿Por qué es útil en Android?
En Android no debes bloquear el hilo principal. Si haces operaciones largas (como acceder a red o base de datos), se necesita una forma de esperar sin colgar la interfaz. Las funciones de suspensión resuelven esto.
- Solo puedes llamar a una
suspend fun
desde otrasuspend fun
o desde dentro de una corrutina (launch
,async
, etc.). - No son “asíncronas” por sí solas: necesitas ejecutarlas dentro de un contexto de corrutina para que lo sean.
- Son muy útiles con
Room
, Retrofit, Firebase, etc.
La corrutinas son livianas.
A diferencia de los hilos (Thread) las corrutinas requieren muy pocos recursos para su creación y mantenimiento en su ejecución, podemos probar de crear 100000 corrutinas con el siguiente código:
fun main(args: Array<String>) { //La corrutinas son livianas. log("Running in the main thread") runBlocking { for (x in 1..100000) launch { delay(1000) print(".") } } log("After runBlocking") }
Podemos observar que no hay problemas de rendimiento en su ejecución, a pesar de hacer creado 100000 corrutinas. Si intentamos hacer lo mismo creando 100000 hilos (Thread) podremos comprobar que se genera un error en la aplicación.
launch devuelve un manejador
Launch devuelve un manejador, un objeto de tipo 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 el código de la corrutina que se crea con launch.
Para esperar, se hace llamando al método join del manejador.
Con Join el hilo espera a que la corrutina termine para continuar su ejecución.
////Manejador que retorna launch log("Running in the main thread") runBlocking { val corrutina1=launch { //crea una corutina y la ejecuta log("En la corrutina 1") delay(1000) log("Pasó un segundo") } //corrutina1.join() //Espera la terminación de la corrutina1 val corrutina2=launch { //crea una corrutina y la ejecuta log("En la corrutina 2") delay(1000) log("Pasó otro segundo") } //corrutina2.join() //Espera la terminación de la corrutina2 } log("After runBlocking")
Aquí hemos podido secuenciar dos corrutinas si utilizamos .join() con el majenador devuelto por launch.
Si comentamos las lineas con .join() veremos que comienzan a la vez y terminan también a la vez (porque los delays son el mismo)
runBlocking y coroutineScope
Cada constructor de corrutinas (runBlocking, launch,…) define un ámbito (alcance o scope) de ejecución de la corrutina.
🔷 ¿Qué es un “scope” (ámbito) de corrutinas?
Un scope es un entorno que gestiona el ciclo de vida de una o más corrutinas.
Determina:
- Dónde se ejecutan.
- Cuándo se cancelan.
- Cuándo se espera que terminen.
Repasemos qué es runBlocking
fun main() { runBlocking { // Dentro de aquí ya puedes usar suspend } }
🔍 ¿Qué hace?
- Crea un scope de corrutina.
- Ejecuta el cuerpo dentro de una corrutina.
- Bloquea el hilo actual (por eso no se usa en Android excepto en
main()
o tests). - Espera a que todo dentro se complete (incluyendo las corrutinas hijas con
launch
oasync
).
Es posible declarar su propio alcance utilizando el constructor de corrutinas coroutineScope.
Éste crea un alcance de corrutina y no se completa hasta que se completan todos los elementos secundarios iniciados.
suspend fun tarea() { coroutineScope { launch { ... } launch { ... } } }
🔍 ¿Qué hace?
- Crea un scope dentro de una función de suspensión.
- No bloquea el hilo, solo suspende la función hasta que todo dentro se complete.
- Se puede usar para lanzar varias corrutinas hijas en paralelo y esperar a que todas terminen.
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:
- El método runBlocking bloquea el hilo actual para esperar
- 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.
Como hemos dicho, se puede utilizar un constructor coroutineScope dentro de cualquier función de suspensión para realizar múltiples operaciones simultáneas. Veamos las diferencias entre runBlocking y corrutineScope
En el main podemos tener:
//runBlocking y coroutineScope runBlocking { Tareas(1) Tareas(2) log("Fin de todas las tareas") }
La función Tareas, es suspend, puesto que vamos a lanzarla desde una corrutina (creada en el main con runBlocking).
Además dentro de la función Tareas vamos a crear dos corrutinas, pero esta vez con el constructor coroutineScope
suspend fun Tareas(nro:Int) { coroutineScope { launch { log("Tarea $nro parte A. iniciando...") delay(1000) log("Tarea $nro parte A. finalizada") } launch { log("Tarea $nro parte B. iniciando...") delay(2000) log("Tarea $nro parte B. finalizada") } log("Esperando finalizar las dos partes de las tareas $nro") } }
Nota: En la ejecución aparecen la Parte A antes que la B puesto que hemos forzado esto con los timings….
Es importante notar que cuando llamamos a:
Tareas(1)
La corrutina de la main se bloquea hasta que finaliza la función de suspensión ‘Tareas’, pero dentro de la función Tareas, cuando se llaman a las corrutinas con launch la función de suspensión continua y espera hasta que todas las corrutinas finalicen.
¿Llamadas concurrentes o paralelas?
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.
Veamos la sintaxis para implementar las llamadas a funciones de suspensión en forma concurrente, podemos usar launch o async.
Ya hemos visto launch, veamos ahora async:
fun main(args: Array<String>) = runBlocking { // LLamadas concurrentes runBlocking { val tiempo1 = System.currentTimeMillis() val corrutina1=async { log("Iniciando la corrutina 1") dato1() } val corrutina2=async { log("Iniciando la corrutina 2") dato2() } println(corrutina1.await()+corrutina2.await()) val tiempo2 = System.currentTimeMillis() println("Tiempo total ${tiempo2-tiempo1} ms") } } suspend fun dato1(): Int { delay(3000) return 3 } suspend fun dato2(): Int { delay(3000) return 3 }
En Composing suspending functions tenemos la explicación de la secuencialidad y la concurrencia en la creación de funciones de suspension.
Conceptualmente, async es como el launch, ambas no bloquean el hilo, inician una corrutina separada que funciona simultáneamente, concurrentemente, con todas las demás corrutinas.
La diferencia es que el launch devuelve un trabajo y no tiene ningún valor resultante, mientras que async devuelve un Deferred (un diferido), un resultado futuro que representa una promesa de proporcionar un resultado más adelante.
Es decir, que async continúa su ejecución pero el resultado que devuelve, ya vendrá…
Puedes usar .await() en un valor diferido (devolución de async) para obtener su resultado final (el Defered).
Un Deferred, también es un Job, por lo que puede cancelarlo si es necesario.
🔄 ¿Concurrente o paralela?
Entonces, ¿lo de llamadas con ejecución paralela? … lo vemos en un momento cuando hablemos de los Dispatchers …
Aquí está la diferencia clave:
Término | Significado técnico |
---|---|
Concurrente | Se intercalan en el tiempo: puede que se ejecuten en el mismo hilo. |
Paralela | Se ejecutan exactamente al mismo tiempo en núcleos distintos. |
👉 async
lanza tareas concurrentes, no necesariamente paralelas.
Si usas un Dispatcher
que usa varios hilos, entonces pueden ejecutarse en paralelo. … Lo vemos en breve.
Lazily started async
Opcionalmente, async se puede hacer lazy (retardado, perezoso), configurando su parámetro de inicio en CoroutineStart.LAZY.
En este modo sólo se inicia la corrutina:
- Cuando await requiere su resultado o
- Si se invoca la función de start del Job.
Mira el siguiente ejemplo:
fun main(args: Array<String>) = runBlocking { // Lazily started async val time = measureTimeMillis { val one = async(start = CoroutineStart.LAZY) { log("Iniciando la corrutina 1") doSomethingUsefulOne() } val two = async(start = CoroutineStart.LAZY) { log("Iniciando la corrutina 2") doSomethingUsefulTwo() } // some computation one.start() // start the first one two.start() // start the second one println("The answer is ${one.await() + two.await()}.") } println("Completed in $time ms") } suspend fun doSomethingUsefulOne(): Int { delay(1000L) // pretend we are doing something useful here return 13 } suspend fun doSomethingUsefulTwo(): Int { delay(1000L) // pretend we are doing something useful here, too return 29 }