Cada aplicación móvil realiza muchas tareas simultáneamente: carga datos de la red, procesa toques del usuario, anima la interfaz y guarda archivos. Si todo este código se ejecuta en un solo hilo, la aplicación se congela ante cualquier retardo de red. La multitarea (multithreading) y la concurrencia son conceptos clave que permiten que la aplicación siga siendo receptiva y eficiente. En este artículo analizaremos todas las herramientas principales: desde Main Thread y RunLoop hasta corrutinas de Kotlin y Combine en iOS. El material se basa en la documentación oficial de Apple GCD.
Puntos Clave
La multitarea es la capacidad de una aplicación para ejecutar varios fragmentos de código simultáneamente. Cada fragmento se ejecuta en un hilo (Thread) separado — un proceso ligero con su propia pila de llamadas. En el desarrollo móvil, los hilos se dividen en dos categorías: Main Thread (hilo de UI) y Background Threads (hilos de fondo).
El sistema operativo gestiona la distribución de los hilos entre los núcleos del procesador. Los dispositivos modernos tienen 6–8 núcleos, por lo que la ejecución paralela puede acelerar el trabajo. Sin embargo, crear hilos es una operación costosa, por lo que no se recomienda trabajar directamente con Thread. En su lugar, se utilizan abstracciones de mayor nivel: DispatchQueue, OperationQueue, CoroutineDispatcher.
La concurrencia es un concepto más amplio que la multitarea. La concurrencia significa que las tareas pueden ejecutarse "simultáneamente" incluso en un solo núcleo mediante el cambio de contexto. La asincronía (Async/Await) es un modelo de programación en el que una tarea no bloquea un hilo, sino que devuelve el control mientras espera un resultado. Los lenguajes modernos (Kotlin, Swift, Dart) tienen soporte integrado para Async/Await.
En IT Sectr prestamos especial atención a la arquitectura correcta de la multitarea al inicio del proyecto. Los errores cometidos en las primeras etapas provocan errores difíciles de detectar: condiciones de carrera, bloqueos mutuos e inestabilidad de la aplicación bajo carga. Cada uno de nuestros proyectos se somete a una revisión de la arquitectura de concurrencia en la etapa de planificación.
El Main Thread (hilo principal) es el único hilo en una aplicación móvil que tiene acceso a la UI. En Android se llama UI Thread, en iOS — Main Thread. Todas las operaciones con la interfaz — cambiar texto, animaciones, procesar toques — se realizan solo en el Main Thread. Si se realiza una operación pesada (carga de archivos, análisis JSON) en el hilo principal, la interfaz deja de responder. En Android esto provoca ANR (Application Not Responding), en iOS — una "congelación" de la pantalla.
Los Background Threads (hilos de fondo) están destinados a todo lo que no esté relacionado con la UI: solicitudes de red, operaciones con bases de datos, procesamiento de imágenes, criptografía. Una vez finalizado, el resultado se transfiere al Main Thread para su visualización. Cada plataforma proporciona sus propias herramientas para cambiar entre hilos: DispatchQueue.main.async en iOS, runOnUiThread o withContext(Dispatchers.Main) en Android.
RunLoop — el bucle de procesamiento de eventos en el hilo principal de iOS. RunLoop espera eventos (toques, temporizadores, notificaciones) y los distribuye a los controladores correspondientes. En Android el equivalente es Looper, asociado con cada Main Thread. Main Looper extrae infinitamente mensajes de la cola y los pasa a Handler para su procesamiento. Comprender RunLoop y Looper ayuda a evitar fugas de memoria y "tartamudeo" de la interfaz.
Grand Central Dispatch (GCD) es una biblioteca de Apple para gestionar la multitarea a nivel del lenguaje C. GCD trabaja con DispatchQueue — colas de tareas. El desarrollador no crea hilos manualmente; GCD gestiona un grupo de hilos (Thread Pool), distribuyendo las tareas entre los núcleos disponibles del procesador. Las DispatchQueue son de dos tipos: Serial Queue (cola serial — las tareas se ejecutan una tras otra) y Concurrent Queue (cola concurrente — las tareas pueden ejecutarse simultáneamente).
Main DispatchQueue es una cola serial vinculada al hilo principal. Global Queues son colas concurrentes con diferentes prioridades (QoS — Quality of Service): userInteractive, userInitiated, utility, background. Elegir el QoS correcto es crítico para el rendimiento: .userInteractive — para tareas que afectan a la UI (animaciones, renderizado); .background — para tareas no críticas en tiempo (sincronización, limpieza de caché).
OperationQueue es una capa superior sobre GCD con capacidades adicionales: cancelación de tareas, establecimiento de dependencias entre operaciones, control del número máximo de operaciones concurrentes. Las operaciones son objetos de la clase Operation (o BlockOperation). Ejemplo: si necesita cargar una imagen, luego aplicar un filtro y solo entonces mostrarla — OperationQueue con dependencias lo maneja perfectamente. En GCD tendría que sincronizar manualmente estos pasos usando DispatchGroup o semáforo.
Async/Await en Swift 5.5+ es una alternativa moderna a GCD. Las palabras clave async y await hacen que el código asíncrono sea lineal y legible. Las funciones se marcan como async y las llamadas se esperan con await. El sistema mismo gestiona el cambio de contexto: por defecto, una función async se ejecuta en un hilo de fondo, mientras que la actualización de la UI se ejecuta en MainActor. @MainActor es un atributo que garantiza la ejecución del código en el hilo principal.
Las corrutinas son hilos ligeros para Kotlin desarrollados por JetBrains. A diferencia de los hilos normales, las corrutinas no están vinculadas a un Thread específico. Miles de corrutinas pueden ejecutarse en varios hilos sin una sobrecarga significativa. CoroutineScope gestiona el ciclo de vida de las corrutinas: viewModelScope está vinculado a ViewModel, lifecycleScope — a Activity/Fragment. Cuando se destruye el ámbito, todas las corrutinas hijas se cancelan automáticamente.
Los Dispatchers determinan en qué grupo de hilos se ejecuta la corrutina: Dispatchers.Main — hilo de UI; Dispatchers.IO — para solicitudes de red y operaciones de disco; Dispatchers.Default — para cálculos intensivos de CPU. Para cambiar de dispatcher se usa withContext. Las corrutinas admiten la concurrencia estructurada: cada corrutina tiene un padre, y cuando se cancela el padre, se cancelan todas las corrutinas hijas. Esto evita fugas de memoria y tareas colgadas.
Flow — un flujo de datos asíncrono frío de la biblioteca de corrutinas. Flow emite valores secuencialmente: (1) el productor genera datos, (2) los operadores transforman el flujo, (3) el colector consume el resultado. A diferencia de LiveData, Flow admite cadenas complejas de operadores (map, filter, flatMapConcat, catch) y es completamente seguro para subprocesos. StateFlow y SharedFlow son variantes calientes de Flow, ideales para el estado de la UI y eventos únicos (Snackbar, navegación).
Channel — otra abstracción de corrutinas para pasar datos entre corrutinas. Channel funciona como una cola: un remitente (send) y uno o más receptores (receive). Los canales con búfer (Channel(UNLIMITED), Channel(BUFFERED)) permiten configurar el comportamiento en caso de desbordamiento. Channel se usa a menudo junto con Flow para puentear APIs basadas en callbacks a corrutinas: callbackFlow { … }.
En IT Sectr usamos activamente corrutinas y Flow en todos los proyectos de Android. Esto permite escribir código asíncrono que parece síncrono, es fácil de probar (runTest, TestDispatcher) y no requiere gestión manual de hilos. Ejemplo de una corrutina simple con carga de datos:
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
suspend fun getUsers(): List<User> = withContext(Dispatchers.IO) {
return@withContext try {
val users = api.fetchUsers()
dao.insertAll(users)
users
} catch (e: Exception) {
dao.getAll()
}
}
}
La programación reactiva es un paradigma en el que los datos se propagan como flujos asíncronos (Observable, Publisher). RxJava/RxKotlin es la implementación más popular para Android, portada desde .NET Rx. RxSwift es una biblioteca similar para iOS. Componentes principales: Observable (fuente de eventos), Observer (suscriptor), Scheduler (gestión de hilos), Operators (transformación de flujos).
Combine es un framework de Apple para programación reactiva presentado en iOS 13. Combine utiliza los protocolos Publisher (editor) y Subscriber (suscriptor). A diferencia de RxSwift, Combine está integrado en el SDK y estrechamente integrado con SwiftUI. Los operadores en Combine: map, filter, combineLatest, zip, debounce, throttle — cubren la mayoría de los escenarios: desde la vinculación de datos a la UI hasta el debounce de consultas de búsqueda.
Future y Promise — patrones para trabajar con un único resultado asíncrono. Future representa un valor que estará disponible más tarde. Promise es una promesa de proporcionar un valor. En Rx esto es Single (una respuesta exitosa o error), en Combine — Future Publisher. En la práctica, Future/Promise son convenientes para solicitudes únicas a una API, mientras que Observable/Publisher son para flujos continuos (geolocalización, entrada de texto).
Callback y Delegate — patrones clásicos para operaciones asíncronas. Callback es una función pasada como argumento y llamada al completar la operación. Delegate es un objeto que implementa un protocolo con métodos manejadores de eventos. Desventaja: "callback hell" (callbacks anidados) y complejidad en el manejo de errores. NotificationCenter (iOS) y EventBus (Android) — mecanismos de transmisión de eventos útiles para comunicación débilmente acoplada, pero que conducen a dependencias implícitas.
La multitarea abre el acceso a un alto rendimiento, pero al mismo tiempo crea el riesgo de errores difíciles de detectar. Los más comunes: Race Condition (condición de carrera), Deadlock (bloqueo mutuo), Livelock (bloqueo activo) y Starvation (inanición de hilo). Comprender estos problemas es una habilidad esencial para cualquier desarrollador móvil.
Race Condition ocurre cuando dos o más hilos leen y escriben simultáneamente los mismos datos sin sincronización. El resultado depende de qué hilo se ejecute primero. Ejemplo clásico: dos hilos incrementan un contador. La operación "leer → incrementar → escribir" no es atómica, por lo que al ejecutarse simultáneamente, un incremento se "pierde". La solución es usar operaciones atómicas (AtomicInteger, AtomicReference) o bloqueos (Mutex, Semaphore, synchronized).
Deadlock — situación en la que cada hilo mantiene un recurso y espera un recurso ocupado por otro hilo. Ningún hilo puede continuar. Condiciones de ocurrencia: exclusión mutua, retención y espera, sin desalojo, espera circular. Prevención: establecer un orden único de adquisición de bloqueos, usar tryLock con tiempo de espera, aplicar algoritmos Lock-Free (ConcurrentHashMap, CopyOnWriteArrayList).
Livelock — los hilos no están bloqueados, pero constantemente "pasan" recursos entre sí sin realizar trabajo útil. Ejemplo: dos personas se encuentran en un pasillo y ambas se hacen a un lado, moviéndose en la misma dirección. Starvation — un hilo no obtiene acceso a un recurso porque otros hilos lo interceptan constantemente. Solución: bloqueos justos (fair locks), prioridades de hilos con precaución.
Para prevenir problemas de multitarea se utilizan primitivas de sincronización: Mutex (exclusión mutua), Semaphore (limitación del número de accesos simultáneos), Lock (interfaz con tryLock), Synchronized (bloqueo a nivel JVM), @MainActor (Swift — garantía de ejecución en el hilo principal). En Android también está disponible ThreadPool (grupo de hilos) a través de Executors.newFixedThreadPool, newCachedThreadPool. Sin embargo, la gestión manual de grupos es prerrogativa de proyectos heredados; en proyectos nuevos es mejor usar corrutinas.
| Herramienta | Plataforma | Tipo | Características |
|---|---|---|---|
| DispatchQueue (GCD) | iOS | Cola de tareas | Serial/Concurrent, prioridades QoS, Thread Pool gestionado por el sistema |
| OperationQueue | iOS | Cola de operaciones | Dependencias, cancelación, maxConcurrentOperationCount |
| Coroutines + Flow | Android | Corrutinas | Ligeras, concurrencia estructurada, StateFlow, Channel |
| RxJava / RxKotlin | Android | Flujo reactivo | Observable, Schedulers, rico conjunto de operadores |
| Combine | iOS | Flujo reactivo | Publisher/Subscriber, integración con SwiftUI |
| Async/Await + Task | iOS / Android | Modelo asíncrono | Código lineal, @MainActor, concurrencia estructurada |
Preguntas Frecuentes
Main Thread (hilo de UI) es responsable de renderizar la interfaz y procesar toques. Background Thread realiza tareas en segundo plano — carga de datos, cálculos, trabajo en red. Bloquear el Main Thread causa congelación de la interfaz (ANR en Android, frozen UI en iOS).
Race Condition es cuando dos hilos acceden simultáneamente a datos compartidos y el resultado depende del orden de ejecución. Se evita mediante sincronización: Mutex, Semaphore, Lock, Synchronized, @MainActor u operaciones atómicas.
Coroutines es el estándar moderno para Android (JetBrains, compatible con Google). RxJava/RxKotlin es un enfoque reactivo con un rico conjunto de operadores. Coroutines es más simple para llamadas asíncronas, RxJava es más potente para flujos de datos complejos. En IT Sectr usamos Coroutines + Flow para proyectos nuevos.
Deadlock es un bloqueo mutuo donde dos hilos esperan los recursos del otro. Livelock — los hilos no están bloqueados, pero pasan recursos constantemente sin hacer trabajo útil. Ambos problemas se resuelven con un orden adecuado de bloqueos y tiempos de espera.
DispatchQueue es una abstracción de Grand Central Dispatch (GCD) para gestionar hilos. Main Queue ejecuta tareas en el hilo principal, Global Queues — en hilos de fondo. Serial Queue garantiza la ejecución secuencial, Concurrent Queue — paralela. En proyectos modernos, GCD a menudo se reemplaza por Async/Await y Task.
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.