Concurrencia y multihilo en Kotlin: nivel avanzado
En el desarrollo moderno, la gestión eficiente de la concurrencia es clave para crear aplicaciones rápidas y receptivas. Kotlin, gracias al soporte integrado de corrutinas (coroutines), ofrece un conjunto de herramientas potente y flexible que va mucho más allá de los hilos estándar de Java. En este artículo, exploraremos conceptos avanzados de concurrencia y multihilo en Kotlin, incluyendo el trabajo con Channel, Flow, Mutex y los detalles de los planificadores.
1. Corrutinas: más allá de lo básico
Las corrutinas son hilos ligeros que permiten escribir código asíncrono en un estilo imperativo. Sin embargo, a nivel avanzado es importante entender cómo funcionan los despachadores (Dispatchers) y los contextos de las corrutinas.
Planificadores (Dispatchers)
Elegir el despachador correcto es crítico para el rendimiento:
Dispatchers.Default— para tareas intensivas en CPU (por defecto usa un pool de hilos igual al número de núcleos del procesador).Dispatchers.IO— para operaciones de entrada/salida (red, archivos). El pool de hilos puede expandirse hasta 64 hilos (o más, según la configuración).Dispatchers.Main— para el hilo de UI (Android, JavaFX).newSingleThreadContextynewFixedThreadPoolContext— para crear pools propios.
import kotlinx.coroutines.*
fun main() = runBlocking { launch(Dispatchers.Default) { println("Tarea intensiva en CPU en ${Thread.currentThread().name}") } launch(Dispatchers.IO) { println("Tarea de E/S en ${Thread.currentThread().name}") }}Concurrencia estructurada
Una de las principales características de Kotlin es la concurrencia estructurada. Garantiza que las corrutinas hijas finalicen antes de que finalice la corrutina padre. Esto previene fugas de memoria y simplifica la gestión del ciclo de vida.
fun main() = runBlocking { coroutineScope { launch { delay(1000) println("Tarea 1 completada") } launch { delay(500) println("Tarea 2 completada") } } println("Todas las tareas completadas")}2. Canales (Channel) para transferir datos entre corrutinas
Channel es un análogo de una cola bloqueante (BlockingQueue), pero para corrutinas. Permite transferir datos de forma segura entre corrutinas, suspendiendo al emisor si el canal está lleno, o al receptor si el canal está vacío.
Tipos de canales
- RendezvousChannel (por defecto) — no tiene búfer. El envío se bloquea hasta que se recibe.
- BufferedChannel — tiene un búfer de tamaño definido.
- ConflatedChannel — almacena solo el último valor enviado, sobrescribiendo los anteriores.
- UnlimitedChannel — búfer ilimitado (cuidado: puede causar OOM).
import kotlinx.coroutines.*import kotlinx.coroutines.channels.*
fun main() = runBlocking { val channel = Channel<Int>(capacity = 5) // Canal con búfer
launch { for (i in 1..10) { channel.send(i) println("Enviado: $i") } channel.close() }
launch { for (value in channel) { println("Recibido: $value") delay(100) } }}Fan-out y Fan-in
Los canales son ideales para los patrones "fan-out" (un productor, varios consumidores) y "fan-in" (varios productores, un consumidor).
3. Flow: flujos de datos reactivos
A diferencia de Channel, que es "caliente" (los datos se envían independientemente de si hay un suscriptor), Flow es un flujo "frío" que