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.
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.
Clases genéricas con varianza y restricciones
Introducción a las corutinas
Uso de asincronía con suspend, async y await
Formateo de cadenas texto
Uso de monads y manejo funcional de errores
Declaración y uso de variables y constantes
Uso de la concurrencia funcional con corutinas
Operaciones en colecciones
Uso de clases y objetos en Kotlin
Evaluación Kotlin
Funciones de orden superior y expresiones lambda en Kotlin
Herencia y polimorfismo en Kotlin
Inmutabilidad y datos inmutables
Uso de funciones parciales y currificaciones
Primer programa en Kotlin
Introducción a la programación funcional
Introducción a Kotlin
Uso de operadores y expresiones
Sistema de inventario de tienda
Uso de data classes y destructuring
Composición de funciones
Uso de interfaces y clases abstractas
Simulador de conversión de monedas
Programación funcional y concurrencia
Creación y uso de listas, conjuntos y mapas
Transformación en monads y functors
Crear e invocar funciones
Uso de las estructuras de control
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
Introducción Y Entorno
Instalación Y Primer Programa De Kotlin
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Cadenas De Texto Y Manipulación
Sintaxis
Estructuras De Control
Sintaxis
Funciones Y Llamada De Funciones
Sintaxis
Clases Y Objetos
Programación Orientada A Objetos
Herencia Y Polimorfismo
Programación Orientada A Objetos
Interfaces Y Clases Abstractas
Programación Orientada A Objetos
Data Classes Y Destructuring
Programación Orientada A Objetos
Tipos Genéricos Y Varianza
Programación Orientada A Objetos
Listas, Conjuntos Y Mapas
Estructuras De Datos
Introducción A La Programación Funcional
Programación Funcional
Funciones De Primera Clase Y De Orden Superior
Programación Funcional
Inmutabilidad Y Datos Inmutables
Programación Funcional
Composición De Funciones
Programación Funcional
Monads Y Manejo Funcional De Errores
Programación Funcional
Operaciones Funcionales En Colecciones
Programación Funcional
Transformaciones En Monads Y Functors
Programación Funcional
Funciones Parciales Y Currificación
Programación Funcional
Introducción A Las Corutinas
Coroutines Y Asincronía
Asincronía Con Suspend, Async Y Await
Coroutines Y Asincronía
Concurrencia Funcional
Coroutines Y Asincronía
Evaluación
Evaluación
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
- Comprender qué son las corutinas y sus ventajas frente a los hilos tradicionales.
- Aprender a crear y usar corutinas empleando constructores como
launch
yasync
. - Manejar excepciones y aplicar timeouts en corutinas.
- Entender cómo administrar los scopes y contextos de ejecución.
- Implementar concurrencia estructurada y gestionar la cancelación de corutinas.
- Optimizar el uso de
Dispatchers
en diferentes escenarios.