Thread es la unidad básica de tiempo de CPU, con su propia pila y ejecutándose independientemente de otros hilos. En el desarrollo móvil, los hilos se utilizan para la ejecución paralela de tareas, manteniendo la interfaz receptiva durante operaciones largas. Android soporta java.lang.Thread, Executors y Kotlin Coroutines, iOS — Thread (Objective-C), GCD y OperationQueue. Según la Documentación de Android Thread, la creación de un hilo nativo requiere asignar ~1 MB para la pila por el sistema operativo.
Puntos Clave
Thread (hilo de ejecución) es una secuencia independiente de instrucciones que el sistema operativo puede planificar en un núcleo de CPU. Cada proceso (aplicación) contiene al menos un hilo — el Hilo Principal. Se crean hilos adicionales para la ejecución paralela de tareas. Cada hilo tiene su propia pila de programa (con variables locales), contador de programa (PC) y registros. La memoria heap es compartida entre todos los hilos del proceso.
En los sistemas operativos móviles, los hilos se planifican mediante multitarea preventiva: el SO puede interrumpir la ejecución de un hilo en cualquier momento y pasar el control a otro (cambio de contexto). El cambio de contexto es una operación costosa (1–10 microsegundos) ya que requiere guardar/restaurar registros de CPU, actualizar el TLB y vaciar los cachés. Por eso un número excesivo de hilos (cientos o miles) degrada el rendimiento — el SO dedica más tiempo a cambiar que a ejecutar.
Hilo vs Proceso — conceptos diferentes. Un proceso es una instancia de una aplicación con memoria virtual asignada. Un hilo dentro de un proceso comparte esta memoria con otros hilos. En Android, cada componente de la aplicación (Activity, Service, BroadcastReceiver) trabaja en un proceso pero puede ejecutarse en diferentes hilos. Una aplicación iOS también es un único proceso capaz de crear hilos adicionales a través de GCD o Thread.
Cada hilo en Java/Kotlin (Android) y NSThread (iOS) pasa por cinco estados: New (creado), Runnable (listo para ejecutar), Running (ejecutándose en CPU), Blocked/Waiting (esperando un recurso o notificación), Terminated (finalizado). Las transiciones entre estados son gestionadas por el planificador del SO y los primitivos de sincronización. El desarrollador puede influir en la prioridad del hilo (Thread.setPriority()) y su estado (sleep, join, interrupt).
En Android, un hilo entra en estado Blocked al intentar adquirir un monitor ocupado (synchronized), llamar a Object.wait() o Thread.sleep(). En iOS — al llamar a NSCondition.wait(), pthread_cond_wait() o dispatch_semaphore_wait(). En estado Blocked, el hilo no consume CPU pero sigue ocupando memoria (pila). Un hilo puede ser interrumpido desde otro hilo, recibiendo InterruptedException (Java) o verificando isCancelled (Kotlin Coroutines).
| Estado | Descripción | Método de Transición |
|---|---|---|
| New | Hilo creado pero no iniciado | Constructor Thread() |
| Runnable | Hilo listo para ejecutar, esperando CPU | thread.start() |
| Running | Hilo ejecutándose en núcleo de CPU | Planificador del SO |
| Blocked/Waiting | Hilo esperando un recurso, monitor o notificación | synchronized, wait(), sleep() |
| Terminated | Hilo completó run() o fue interrumpido | run() completado, interrupt() |
Cambio de contexto es una operación donde el SO guarda el estado del hilo actual (registros, PC, TLB) y carga el estado guardado de otro. En sistemas móviles (Linux + ART, XNU para iOS) un cambio de contexto toma 1–10 microsegundos. Si un hilo ejecuta una tarea en 100 microsegundos y un cambio de contexto toma 5, entonces se pierde el 5% del tiempo. Para minimizar los cambios de contexto, iOS usa GCD con work stealing, Android usa pools con fixedThreadCount.
Android ha evolucionado desde java.lang.Thread de bajo nivel hasta las corrutinas modernas. Cada nivel de abstracción proporciona más capacidades con menos sobrecarga. Thread es la clase base, pero no se recomienda su creación directa: un nuevo hilo no es gestionado por un pool, es difícil de monitorear y cancelar. AsyncTask (obsoleto desde API 30) fue un paso adelante, pero sufría de fugas de memoria y manejo incómodo de configuraciones.
HandlerThread es una subclase especial de Thread con Looper que puede procesar una cola de mensajes. Se usa para la ejecución secuencial de tareas en un hilo de fondo, por ejemplo, escribiendo datos en Room o archivos. HandlerThread se crea llamando a start(), después de lo cual se pueden enviar mensajes y Runnable a través de Handler(handlerThread.looper). Llamar a handlerThread.quit() detiene el Looper y termina el hilo.
// Android: Thread, HandlerThread y Executors
import android.os.Handler
import android.os.HandlerThread
import java.util.concurrent.Executors
class ThreadExample {
// 1. Creación directa de Thread (no recomendada)
fun directThread() {
val thread = Thread(Runnable {
Thread.sleep(1000)
print("Direct thread executed")
})
thread.start()
}
// 2. HandlerThread para tareas secuenciales en segundo plano
fun handlerThreadExample() {
val handlerThread = HandlerThread("BackgroundQueue")
handlerThread.start()
val handler = Handler(handlerThread.looper)
handler.post {
// Ejecución secuencial en un hilo de fondo
Thread.sleep(500)
print("HandlerThread: tarea completada")
}
// Detener el hilo (se ejecuta cuando las tareas terminan)
handlerThread.quitSafely()
}
// 3. Executors — pool de hilos
fun executorExample() {
val executor = Executors.newFixedThreadPool(4)
for (i in 1..10) {
executor.execute {
print("Task $i on thread ${Thread.currentThread().getName()}")
}
}
executor.shutdown()
}
// 4. Kotlin Coroutines — estándar moderno
suspend fun coroutineExample() = kotlinx.coroutines.withContext(
kotlinx.coroutines.Dispatchers.Default
) {
print("Coroutine on thread: ${Thread.currentThread().getName()}")
}
}
El ejemplo ThreadExample muestra los cuatro niveles de abstracción de hilos en Android. La creación directa de Thread es el enfoque más bajo y menos eficiente. HandlerThread es útil para tareas secuenciales en segundo plano. Executors.newFixedThreadPool(4) crea un pool de 4 hilos para la ejecución paralela de hasta 10 tareas. Kotlin Coroutines con Dispatchers.Default es un enfoque moderno, eficiente y seguro.
HandlerThread es una subclase especializada de Thread con un Looper incorporado y cola de mensajes. Se crea llamando a start(), después de lo cual se pueden enviar Runnable y mensajes a través de Handler(handlerThread.looper). HandlerThread ejecuta tareas estrictamente secuenciales — la siguiente tarea no comienza hasta que la anterior se completa. Esto es útil para escribir datos en Room o archivos, donde el orden de las operaciones es crítico. Llamar a quitSafely() detiene el Looper después de que la tarea actual se completa.
iOS también proporciona tres niveles de gestión de hilos. Thread (Thread en Swift, NSThread en Objective-C) es una API de bajo nivel que crea directamente un hilo nativo. GCD (Grand Central Dispatch) a través de DispatchQueue es la herramienta principal para los desarrolladores de iOS, gestionando automáticamente un pool de hilos. OperationQueue es una abstracción de alto nivel sobre GCD con soporte para dependencias, prioridades y cancelación.
El uso directo de Thread en el desarrollo moderno de iOS es extremadamente raro — GCD proporciona todas las capacidades necesarias con gestión automática de memoria e hilos. Thread se usa solo para casos específicos: almacenamiento local de hilo (threadDictionary), creación de un RunLoop para un hilo de fondo, o integración con bibliotecas C que esperan pthread_t.
import Foundation
class ThreadManager {
// 1. Thread (bajo nivel)
func createThread() {
let thread = Thread {
// Código ejecutándose en un nuevo hilo
print("Current thread: \(Thread.current)")
}
thread.name = "com.app.worker"
thread.qualityOfService = .utility
thread.start()
}
// 2. GCD — DispatchQueue
func gcdExample() {
// Cola concurrente
let queue = DispatchQueue(label: "com.app.concurrent",
qos: .utility,
attributes: .concurrent)
queue.async {
print("GCD async task")
}
// Barrera para sincronización de escritura
queue.async(flags: .barrier) {
// Acceso exclusivo durante la escritura
print("Barrier write: exclusive access")
}
}
// 3. OperationQueue con dependencias
func operationQueueExample() {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .background
let download = BlockOperation {
print("Downloading...")
}
let process = BlockOperation {
print("Processing...")
}
let save = BlockOperation {
print("Saving...")
}
// Dependencias: download -> process -> save
process.addDependency(download)
save.addDependency(process)
queue.addOperations([download, process, save], waitUntilFinished: false)
}
}
// Colección thread-safe mediante barrera GCD
class ThreadSafeArray<T> {
private var array: [T] = []
private let queue = DispatchQueue(label: "com.app.concurrent",
attributes: .concurrent)
var count: Int {
return queue.sync { array.count } // concurrent read
}
func append(_ element: T) {
queue.async(flags: .barrier) { // exclusive write
self.array.append(element)
}
}
}
La clase ThreadSafeArray demuestra el patrón de Lectura Concurrente / Escrita Exclusiva mediante la barrera de GCD. La lectura a través de queue.sync{} se ejecuta en paralelo desde múltiples hilos. La escritura a través de queue.async(flags: .barrier) bloquea todas las demás operaciones (tanto lecturas como escrituras) hasta que la escritura se completa. Esto es más eficiente que los bloques sincronizados ya que no bloquea a los lectores cuando no hay escritura.
El uso directo de Thread en iOS se justifica en tres casos: para almacenamiento local de hilo (Thread.current.threadDictionary) — guardar datos vinculados a un hilo; para crear un RunLoop especial en un hilo de fondo con performSelector:onThread:; para integración con bibliotecas C/C++ que esperan pthread_t. En todos los demás casos, GCD a través de DispatchQueue es preferible — gestiona automáticamente el pool de hilos y el consumo de energía.
Race condition ocurre cuando dos o más hilos acceden simultáneamente a datos compartidos y al menos uno está escribiendo. El resultado depende del tiempo de ejecución y es impredecible. Se utilizan primitivos de sincronización para prevenir race conditions. En el desarrollo móvil, los primitivos disponibles incluyen locks (synchronized, NSLock), operaciones atómicas (AtomicInteger, propiedades atómicas de iOS) y colas (cola serial).
Selección del primitivo depende del escenario. Para contadores y banderas simples, las operaciones atómicas son suficientes (AtomicInteger, propiedad atómica). Para secciones críticas con múltiples operaciones — locks (synchronized, NSLock). Para estructuras de datos complejas — DispatchQueue serial o barrera GCD. Los locks son más fáciles de entender pero propensos a deadlocks y livelocks. Las colas son más complejas pero más seguras.
// Sincronización en Android/Kotlin
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class Counter {
// 1. AtomicInteger — para contadores simples
private val atomicCount = AtomicInteger(0)
fun incrementAtomic() = atomicCount.incrementAndGet()
// 2. synchronized — para secciones críticas
@Synchronized
fun synchronizedOperation() {
// Solo un hilo a la vez
doWork()
}
// 3. Mutex de corrutina — suspend-safe
private val mutex = Mutex()
suspend fun mutexOperation() {
mutex.withLock {
// protected code — thread-safe
doWork()
}
}
private fun doWork() { /* critical section */ }
}
// Ejemplo de Deadlock: A bloquea B, B bloquea A
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun methodA() = synchronized(lockA) {
Thread.sleep(100)
synchronized(lockB) { print("OK") }
}
fun methodB() = synchronized(lockB) {
Thread.sleep(100)
synchronized(lockA) { print("OK") }
}
}
Counter demuestra tres enfoques de sincronización. AtomicInteger.incrementAndGet() — operación atómica sin locks (CAS). @Synchronized — monitor incorporado de Java, bloquea todo el objeto. Mutex.withLock — mutex de corrutina, suspende la corrutina en lugar de bloquear el hilo (más eficiente). DeadlockExample muestra un deadlock clásico: dos hilos adquieren locks en diferente orden.
Thread Pool es un conjunto de hilos precreados que se reutilizan para la ejecución de tareas. En lugar de crear un nuevo hilo para cada tarea (costoso), el pool toma un hilo libre del pool. Si no hay hilos libres, la tarea se pone en cola. El pool gestiona automáticamente su tamaño: se crean nuevos hilos bajo cargas pico, los hilos inactivos se terminan. Esto reduce la sobrecarga de creación de hilos en decenas de veces.
En Android, Executors.newFixedThreadPool(4) crea un pool de 4 hilos. Si llegan 10 tareas simultáneamente, 4 comienzan a ejecutarse inmediatamente, 6 esperan en la cola. Executors.newCachedThreadPool() crea hilos según sea necesario (sin límite) y termina los inactivos después de 60 segundos. Para iOS, GCD proporciona automáticamente pools de colas globales cuyo tamaño corresponde al número de núcleos de CPU y la carga actual.
En Kotlin Coroutines, los pools de hilos están ocultos dentro de los dispatchers. Dispatchers.Default usa un pool de tamaño = número de núcleos de CPU (mínimo 2). Dispatchers.IO — 64 hilos (suficiente para cientos de tareas IO-bound ya que la mayoría estará esperando E/S, sin ocupar CPU). Cada dispatcher escala automáticamente el pool bajo carga, ahorrando batería en inactividad.
Preguntas Frecuentes
Thread es la unidad básica de ejecución de código en una aplicación. Cada proceso puede tener múltiples hilos que comparten memoria pero con su propia pila. En el desarrollo móvil, los hilos se utilizan para la ejecución paralela de tareas sin bloquear la UI. Android usa Thread, Executors, HandlerThread y Coroutines. iOS usa Thread, GCD (DispatchQueue) y OperationQueue.
Crear un Thread requiere asignar ~1 MB de pila en Android y ~512 KB en iOS — es una operación costosa. Para 1000 tareas, crear directamente 1000 hilos requeriría ~1 GB solo para pilas más la sobrecarga del cambio de contexto. En lugar de Thread, use pools (Executors, GCD) o corrutinas — reutilizan hilos, reduciendo la sobrecarga en decenas de veces.
Race condition es un comportamiento impredecible cuando múltiples hilos acceden simultáneamente a datos compartidos con escrituras. Se puede evitar de tres maneras: usar tipos atómicos (AtomicInteger), locks (synchronized, NSLock) o serializar el acceso a través de una cola (DispatchQueue serial, Actor en Kotlin). La mejor práctica es minimizar el estado mutable compartido y usar inmutabilidad.
Thread es un objeto nativo del sistema que ocupa ~1 MB de pila y está vinculado al núcleo del SO. Corrutina es una unidad de ejecución ligera de Kotlin que no está vinculada a un hilo específico y puede suspenderse sin bloquear. Un hilo puede ejecutar miles de corrutinas. Las corrutinas son más eficientes en memoria y permiten escribir código asíncrono sin callbacks.
Deadlock se manifiesta como una congelación completa de la aplicación sin ANR. En Android, use Thread.getAllStackTraces() para volcar todas las pilas de hilos — dos hilos estarán esperando los locks del otro. En iOS — Thread.callStackSymbols. Herramientas: Android Studio Profiler (pestaña Threads), Instruments (iOS, Thread State View). Prevención: adquiera locks en un orden fijo, use tryLock con tiempo de espera.
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