Si alguna vez te has peleado con el rendimiento de una aplicación, sabrás que el gran quebradero de cabeza suelen ser esas tareas que tardan una eternidad, como leer archivos pesados o hacer peticiones a un servidor. Aquí es donde entran en juego las corrutinas, una herramienta alucinante que nos permite gestionar la concurrencia de forma eficiente sin que la interfaz de usuario se quede congelada y el usuario se desespere.
Básicamente, estamos hablando de una manera de escribir código asíncrono que parece síncrono. En lugar de liarnos con una maraña de funciones que se llaman unas a otras, las corrutinas nos dejan pausar y reanudar la ejecución de una tarea, permitiendo que el procesador haga otras cosas mientras esperamos una respuesta. Es, en esencia, una forma de aprovechar al máximo los recursos del sistema sin complicarnos la vida más de la cuenta.
El mundo de las Corrutinas en Python
Python, al ser un lenguaje de un solo hilo debido al famoso Global Interpreter Lock (GIL), necesita trucos para no bloquearse. Para ello utiliza el event loop, que es como un director de orquesta que decide qué tarea se ejecuta y cuál se queda esperando en una cola de prioridades. La librería asyncio es la pieza clave aquí, introduciendo la sintaxis async y await.
Cuando definimos una función con async def, creamos una corrutina. Pero ojo, que llamarla no hace que se ejecute mágicamente; necesitamos añadirla al bucle de eventos, normalmente mediante asyncio.run(). Si queremos que varias tareas corran a la vez, tenemos opciones como asyncio.gather() o la más moderna asyncio.TaskGroup, que permite gestionar grupos de tareas de forma estructurada, cancelándolas todas si una de ellas falla.
Dentro de este ecosistema, encontramos los objetos esperables (awaitables), que se dividen principalmente en corrutinas, tareas y futuros. Mientras que las tareas se usan para programar la ejecución concurrente mediante create_task(), los Futures son representaciones de un resultado eventual que aún no ha llegado, siendo muy útiles cuando trabajamos con código antiguo basado en callbacks.
Dominando las Corrutinas en Kotlin y Android
En el ecosistema de Android, las corrutinas son la salvación para evitar el temido «callback hell», esa situación donde el código se va indentando hacia la derecha infinitamente porque una petición depende de la anterior. Kotlin propone las funciones de suspensión, marcadas con la palabra suspend, que pueden detener la ejecución de una corrutina sin bloquear el hilo donde están corriendo, evitando así que se produzcan errores ANR, crash y kernel panic en Android.
Para que esto funcione, necesitamos un CoroutineScope, que es el ámbito que define el ciclo de vida de la tarea. Tenemos el GlobalScope para tareas que deben vivir mientras la app esté abierta, el lifecycleScope vinculado a la vista y el viewModelScope, ideal para operaciones de datos en el ViewModel. Si necesitamos cambiar el hilo sobre la marcha, usamos withContext, que es una función de suspensión que nos permite saltar entre diferentes contextos de ejecución.
Los Dispatchers: ¿Dónde corre mi código?
El dispatcher es el que decide en qué hilo se ejecuta la corrutina. No todos son iguales y elegir el correcto es vital para que la app no pete:
- Default: Optimizado para tareas que consumen mucha CPU, como cálculos matemáticos o procesar JSONs complejos.
- IO: El mejor amigo de las peticiones de red y acceso a bases de datos, ya que gestiona un pool de hilos diseñado para esperar respuestas externas.
- Main: Reservado estrictamente para interactuar con la interfaz de usuario en Android.
- Unconfined: No está vinculado a ningún hilo específico, por lo que su comportamiento es impredecible y debe usarse con cautela.
Constructores: launch vs async
Para arrancar una corrutina usamos builders. El más común es launch, que dispara una tarea y nos devuelve un objeto Job. Con este Job podemos hacer un join() para esperar a que termine o un cancel() para abortar la operación. Por otro lado, tenemos async, que es la joya de la corona cuando necesitamos un resultado. async devuelve un Deferred, y mediante el método await() podemos recuperar el valor final.
La diferencia real es que mientras launch es para tareas de «disparar y olvidar», async permite ejecutar varias tareas en paralelo y luego sincronizar sus resultados, reduciendo drásticamente el tiempo total de ejecución. Por ejemplo, si tenemos dos peticiones que tardan 2 segundos cada una, ejecutarlas secuencialmente tardaría 4, pero con async tardarían solo 2 en total.
De los Callbacks a la Programación Secuencial
Tradicionalmente, para manejar la asincronía usábamos callbacks: pasábamos una función como parámetro que se ejecutaba al terminar la tarea. Aunque funciona, esto hace que el código sea difícil de leer y mantener. La transición a corrutinas implica transformar esos métodos en funciones de suspensión.
En Android, esto se logra a menudo usando suspendCancellableCoroutine o suspendCoroutine, que actúan como un puente. Estos bloques nos dan un objeto continuation, el cual nos permite decirle a la corrutina: «espera aquí hasta que el callback de la librería externa responda, y entonces reanuda la ejecución con este valor». Así, el ViewModel puede llamar a una función de repositorio de forma lineal, envolviéndola en un bloque try-catch para gestionar los errores de red de forma natural.
Las corrutinas son increíblemente ligeras comparadas con los hilos tradicionales. Podemos lanzar miles de ellas sin saturar la memoria, ya que no crean un hilo del sistema operativo por cada instancia, sino que reutilizan hilos existentes mediante la suspensión. Esto permite que aplicaciones complejas mantengan una fluidez total, delegando la carga pesada al dispatcher de IO y regresando al hilo Main solo para actualizar la pantalla. Comparte este artículo y más usuarios sabrán la información