Dispatchers en Kotlin Coroutines son componentes de CoroutineContext que determinan los hilos para ejecutar corrutinas: Main (hilo de UI), IO (red y disco), Default (tareas intensivas de CPU) y Unconfined (hilo actual). Cada dispatcher gestiona un grupo de hilos especializado optimizado para un tipo específico de trabajo. Según la guía de JetBrains, 2024, elegir el dispatcher correcto es crítico para el rendimiento y la estabilidad de la aplicación.
Puntos Clave
Dispatchers son implementaciones de la interfaz CoroutineDispatcher, que son elementos de CoroutineContext. Determinan en qué hilo o grupo de hilos se ejecutará la corrutina. Al crear una corrutina mediante launch o async, el dispatcher se puede pasar como primer parámetro: launch(Dispatchers.IO) { ... }. Si no se especifica ningún dispatcher, se hereda del CoroutineScope externo.
Kotlin proporciona cuatro dispatchers incorporados: Main, IO, Default, Unconfined. Cada dispatcher utiliza su propio grupo de hilos optimizado para un tipo específico de operación. Elegir el dispatcher correcto determina el rendimiento de la aplicación: una elección incorrecta provoca retrasos en la UI, núcleos de CPU inactivos o uso ineficiente de hilos.
| Dispatcher | Grupo de Hilos | Máx. Hilos | Uso |
|---|---|---|---|
| Dispatchers.Main | Uno (UI) | 1 | Actualizaciones de UI, LiveData, View |
| Dispatchers.IO | Grupo IO | 64 (limitedParallelism) | Red, archivos, BD |
| Dispatchers.Default | Grupo CPU | N núcleos | Ordenación, análisis, cálculos |
| Dispatchers.Unconfined | Hilo actual | N/A | Operaciones intermedias, pruebas |
Dispatchers.Main es el dispatcher que ejecuta corrutinas en el hilo principal de Android. Está diseñado para operaciones relacionadas con la interfaz: actualizar TextView, llamar a notifyDataSetChanged, trabajar con LiveData y StateFlow. En Android, este dispatcher se implementa mediante Handler (Looper.getMainLooper()).
// Correct switch to Main for UI updates
viewModelScope.launch(Dispatchers.IO) {
val data = repository.fetchData()
withContext(Dispatchers.Main) {
_uiState.value = data
}
}
Si una corrutina ya está en el dispatcher Main, un withContext(Dispatchers.Main) adicional no genera sobrecarga — el dispatcher verifica el hilo actual y omite el cambio. withContext es la forma preferida de cambiar entre dispatchers.
Dispatchers.IO es un dispatcher optimizado para operaciones de E/S: peticiones HTTP (Ktor, OkHttp), lectura y escritura de archivos, trabajo con Room o SQLDelight. Utiliza un grupo de 64 hilos por defecto, escalable bajo carga. Cada nueva petición de E/S puede crear un hilo adicional hasta alcanzar el límite.
Para controlar el número de operaciones de E/S concurrentes, use limitedParallelism(). Esta función crea un nuevo dispatcher con un límite en el número de hilos paralelos, evitando el agotamiento del grupo durante operaciones masivas.
val limitedIo = Dispatchers.IO.limitedParallelism(4)
// Load 100 files with limit of 4 concurrent operations
coroutineScope {
val files = (1..100).map { index ->
async(limitedIo) {
downloadFile("file_$index")
}
}
files.awaitAll()
}
Use el dispatcher IO para todas las operaciones donde la corrutina pasa tiempo esperando (I/O-bound). Las tareas intensivas de CPU en el dispatcher IO son ineficientes — ocupan hilos destinados a E/S, reduciendo el rendimiento del sistema.
Dispatchers.Default es el dispatcher para operaciones de cómputo que cargan el procesador: ordenación, filtrado, análisis JSON (Moshi, Kotlinx Serialization), procesamiento de imágenes, cálculos. El tamaño del grupo equivale al número de núcleos del procesador (pero no menos de 2). Esto garantiza la máxima utilización de la CPU sin cambios de contexto.
suspend fun processData(input: List<RawRecord>): List<ProcessedRecord> {
return withContext(Dispatchers.Default) {
input
.parallelStream()
.map { transform(it) }
.toList()
}
}
No use Dispatchers.Default para operaciones de E/S — esto bloqueará los hilos del grupo de CPU que podrían estar procesando tareas computacionales. La separación de IO y Default permite una utilización óptima de los recursos del sistema: los hilos IO esperan E/S, los hilos CPU están constantemente ocupados con cálculos.
Dispatchers.Unconfined es un dispatcher especial que no vincula una corrutina a ningún grupo. La corrutina comienza la ejecución en el hilo donde se llamó a launch/async, y después de la suspensión se reanuda en el hilo que llamó a resume. Este comportamiento es adecuado para operaciones intermedias que no requieren un contexto fijo.
fun main() = runBlocking {
launch(Dispatchers.Unconfined) {
println("Before delay: ${Thread.currentThread().getName()}")
delay(500L)
println("After delay: ${Thread.currentThread().getName()}")
}
}
En código de producción, Dispatchers.Unconfined se usa raramente. Casos principales: transformaciones ligeras antes de pasar datos a otro dispatcher y pruebas. Para cargas de producción, use dispatchers explícitos — Unconfined es impredecible porque el hilo de ejecución depende de la implementación de resume.
La selección del dispatcher depende del tipo de tarea: operaciones de UI → Main, I/O-bound → IO, CPU-bound → Default, intermedias → heredar del scope. Para Android, se recomienda lanzar una corrutina en el dispatcher donde se realiza el trabajo principal, y cambiar a Main mediante withContext antes de actualizar la UI.
Para escenarios complejos, combine dispatchers usando el operador +: Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler. Esto crea un CoroutineContext con un dispatcher especificado, manejo de errores y una jerarquía de Job aislada.
Preguntas Frecuentes
Dispatchers.IO usa un grupo de hasta 64 hilos para operaciones I/O-bound (espera de E/S), mientras que Dispatchers.Default usa un grupo basado en el número de núcleos de CPU para tareas computacionales. Cuando escasean los hilos, ambos grupos pueden compartir hilos entre sí.
Sí, use newSingleThreadContext() para un hilo único o newFixedThreadPoolContext() para un grupo fijo. Para producción, use limitedParallelism() basado en dispatchers existentes — esto es más eficiente que crear nuevos grupos.
Si Dispatchers.Main no está disponible (por ejemplo, en una prueba JUnit o servicio en segundo plano), se lanza una IllegalStateException. Use TestCoroutineDispatcher para pruebas y Dispatchers.IO o Default para servicios en segundo plano.
Use Dispatchers.IO.limitedParallelism(N), donde N es el número máximo de hilos paralelos. Esto evita el agotamiento del grupo durante solicitudes masivas y proporciona un paralelismo controlado.
Dispatchers.Unconfined es adecuado para operaciones intermedias: transformaciones ligeras de datos antes de pasar a otro dispatcher, escenarios de prueba. En código de producción de Android no se recomienda debido al hilo de ejecución indefinido después de la suspensión.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también