Thread Pool — es un mecanismo de gestión de hilos en el que un grupo de hilos creado previamente se reutiliza para ejecutar tareas, evitando los costos adicionales de crear y destruir hilos. En el desarrollo móvil, el thread pool se utiliza para operaciones en segundo plano: solicitudes de red, procesamiento de imágenes, trabajo con bases de datos. Según la documentación de Google Android (2025), ExecutorService es la forma recomendada de gestionar hilos en segundo plano en Android. En iOS, OperationQueue y GCD DispatchQueue con colas concurrentes globales cumplen un papel similar.
Puntos clave
Thread Pool (grupo de hilos) es un patrón arquitectónico en el que se crea una cantidad fija de hilos de antemano y se reutilizan para ejecutar múltiples tareas. En lugar de crear un nuevo hilo para cada operación (lo que es costoso: aproximadamente 1 MB de pila por hilo en JVM), las tareas se colocan en una cola y son ejecutadas por los hilos disponibles del pool. En el desarrollo móvil, el thread pool es crítico para el rendimiento: Android e iOS limitan la cantidad de hilos por aplicación.
Crear un hilo es una operación costosa: asignación de pila, registro en el sistema, cambio de contexto. En dispositivos móviles con recursos limitados, la creación descontrolada de hilos provoca OOM (OutOfMemoryError) en Android y estrangulamiento en iOS. Thread Pool resuelve ambos problemas: limita la cantidad máxima de hilos que se ejecutan simultáneamente y reutiliza los ya creados. Google recomienda ExecutorService en lugar de raw Thread(), Apple recomienda OperationQueue en lugar de Thread.
| Parámetro | Sin pool (raw Thread) | Con Thread Pool |
|---|---|---|
| Creación de hilo | Para cada tarea | Una vez al crear el pool |
| Máximo de hilos | Sin límite (riesgo de OOM) | Limitado por core/max pool size |
| Utilización | Baja (el hilo muere después de la tarea) | Alta (el hilo se reutiliza) |
| Gestión | Manual (join, interrupt) | Automática (ExecutorService) |
| Consumo de memoria | Crece con cada tarea | Fijo |
El thread pool funciona según el principio Productor-Consumidor: las tareas (Runnable/Callable) se colocan en una cola bloqueante (BlockingQueue). Los hilos del pool esperan tareas en la cola y las toman para ejecutarlas. Algoritmo: si hay menos hilos libres que corePoolSize, se crea un nuevo hilo. Si se alcanza corePoolSize, la tarea se coloca en la cola. Si la cola está llena y hay menos hilos que maximumPoolSize, se crea un hilo adicional. Si se supera maximumPoolSize, la tarea se rechaza mediante RejectedExecutionHandler.
Core pool size — la cantidad de hilos que se mantienen en el pool incluso cuando están inactivos. Maximum pool size — la cantidad máxima de hilos que se puede crear cuando la cola se desborda. La diferencia entre ellos son los hilos adicionales (overflow) que se crean temporalmente y se terminan después del tiempo de inactividad. En dispositivos móviles, se recomienda establecer corePoolSize igual a maximumPoolSize para evitar picos de carga por la creación de hilos.
BlockingQueue almacena tareas que esperan ser ejecutadas. Las implementaciones más populares: LinkedBlockingQueue (ilimitada), ArrayBlockingQueue (limitada) y SynchronousQueue (sin almacenamiento: la tarea se pasa directamente a un hilo). Cuando la cola y el pool están llenos, se activa RejectedExecutionHandler. Las políticas estándar son: AbortPolicy (lanza RejectedExecutionException), CallerRunsPolicy (ejecuta en el hilo del llamante), DiscardPolicy y DiscardOldestPolicy.
// Creación de Thread Pool en Android
val threadPool = ThreadPoolExecutor(
corePoolSize = 2, // Mínimo 2 hilos
maximumPoolSize = 4, // Máximo 4 hilos
keepAliveTime = 30L, // Tiempo de vida del hilo de desbordamiento
unit = TimeUnit.SECONDS,
workQueue = LinkedBlockingQueue<Runnable>(16),
threadFactory = Executors.defaultThreadFactory(),
handler = ThreadPoolExecutor.CallerRunsPolicy()
)
// Envío de tareas
threadPool.execute {
val result = api.fetchData()
runOnUiThread { showData(result) }
}
// Cierre del pool
threadPool.shutdown()
// Espera a que se completen todas las tareas
threadPool.awaitTermination(10, TimeUnit.SECONDS)
Android proporciona varias implementaciones de thread pool a través de java.util.concurrent. Executors — una fábrica con configuraciones predefinidas: newFixedThreadPool(n) (pool fijo), newCachedThreadPool() (ilimitado, los hilos se crean según sea necesario), newSingleThreadExecutor() (un solo hilo: ejecución secuencial). Para proyectos móviles, se recomienda newFixedThreadPool con un límite razonable (2-4 hilos), ya que el pool cached puede crear demasiados hilos.
ThreadPoolExecutor (TPE) — una implementación completa de ExecutorService con parámetros configurables. En Android, TPE se usa internamente en AsyncTask, IntentService y JobIntentService. Los parámetros corePoolSize, maximumPoolSize, keepAliveTime, BlockingQueue y RejectedExecutionHandler permiten ajustar el comportamiento del pool. Recomendaciones para Android: corePoolSize = número de núcleos de CPU - 1 (para tareas IO-bound) o número de núcleos (para tareas CPU-bound). Para aplicaciones típicas: 2-4 hilos.
// Configuraciones predefinidas de Executors
// 1. Pool fijo de 3 hilos
val fixedPool = Executors.newFixedThreadPool(3)
// 2. Pool almacenado en caché (no recomendado para móviles)
val cachedPool = Executors.newCachedThreadPool()
// 3. Hilo único (serialización)
val singlePool = Executors.newSingleThreadExecutor()
// 4. Programador (tareas periódicas)
val scheduler = Executors.newScheduledThreadPool(2)
// Uso con Callable y Future
val future: Future<String> = fixedPool.submit(Callable {
"Result: ${api.call()}"
})
// Obtención del resultado (bloquea el hilo)
val result = future.get(5, TimeUnit.SECONDS)
// Cierre del pool
fixedPool.shutdownNow()
Las corrutinas de Kotlin proporcionan CoroutineDispatcher — una abstracción similar al thread pool. Dispatchers.IO utiliza un pool de 64 hilos (limitado). Dispatchers.Default — un pool igual al número de núcleos de CPU. CoroutineDispatcher no requiere cierre manual y se gestiona automáticamente. Para un ajuste fino, cree un ExecutorCoroutineDispatcher personalizado mediante Executors.newFixedThreadPool(2).asCoroutineDispatcher(). Las corrutinas no reemplazan el thread pool, sino que lo envuelven.
iOS proporciona dos mecanismos principales para gestionar el thread pool: OperationQueue (API de alto nivel basada en GCD) y GCD DispatchQueue (API de bajo nivel en C). OperationQueue encapsula un thread pool a través de la propiedad maxConcurrentOperationCount. Por defecto, OperationQueue utiliza un máximo definido por el sistema (depende de la carga del sistema). DispatchQueue.global() proporciona una cola concurrente con un pool de hilos del sistema.
OperationQueue gestiona el pool de hilos a través de maxConcurrentOperationCount. Un valor de 1 crea una cola serie (similar a un pool de un solo hilo). Un valor mayor que 1 crea un pool concurrente con el límite especificado. Por defecto, maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount (óptimo del sistema, generalmente 4-8 hilos). Operation admite dependencias, prioridades y cancelación. Cada operación se ejecuta en cualquier hilo disponible del pool del sistema.
// OperationQueue con un pool de 3 hilos
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
queue.qualityOfService = .utility
// Creación de operaciones
let operation1 = BlockOperation {
let data = fetchData(from: url1)
DispatchQueue.main.async { updateUI(data) }
}
let operation2 = BlockOperation {
let data = fetchData(from: url2)
DispatchQueue.main.async { updateUI(data) }
}
// Dependencia: operation2 espera a operation1
operation2.addDependency(operation1)
// Adición a la cola
queue.addOperations([operation1, operation2], waitUntilFinished: false)
// Cancelación de todas las operaciones
queue.cancelAllOperations()
DispatchQueue es el pool de hilos de Apple. Una cola concurrente (qos: .utility) utiliza el pool de hilos del sistema, optimizado para la carga actual del dispositivo. Diferentes niveles de QoS (userInteractive, userInitiated, utility, background) se asignan a diferentes pools con diferentes prioridades. DispatchGroup permite sincronizar múltiples tareas. DispatchWorkItem admite cancelación y qualityOfService. Para un control más preciso, cree colas concurrentes personalizadas mediante DispatchQueue(label: qos: attributes: .concurrent).
// GCD DispatchQueue como thread pool
let customQueue = DispatchQueue(
label: "com.app.background",
qos: .utility,
attributes: .concurrent,
autoreleaseFrequency: .workItem
)
// Envío de tareas al pool
customQueue.async { self.processFile(file1) }
customQueue.async { self.processFile(file2) }
// DispatchGroup para sincronización
let group = DispatchGroup()
let pool = DispatchQueue.global(qos: .utility)
pool.async(group: group) { fetchData() }
pool.async(group: group) { processImage() }
group.notify(queue: .main) {
self.showResult() // Ambas tareas completadas
}
// Limitación de concurrencia mediante semáforo
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
pool.async {
semaphore.wait()
download(url)
semaphore.signal()
}
}
La configuración del thread pool afecta directamente el rendimiento de la aplicación. Parámetros incorrectos provocan una subutilización de la CPU (demasiados pocos hilos) o una sobrecarga del sistema (demasiados). Para aplicaciones móviles, los valores óptimos difieren de los del lado del servidor debido a los recursos limitados y el consumo de energía. Los parámetros principales son: corePoolSize, maxPoolSize, capacidad de la cola y keepAliveTime.
Fórmula para tareas IO-bound: corePoolSize = número de núcleos de CPU × 2 (los hilos esperan E/S). Para tareas CPU-bound: corePoolSize = número de núcleos de CPU (los hilos están constantemente ocupados con cálculos). En dispositivos móviles modernos (6-8 núcleos), esto da 6-8 hilos para CPU-bound y 12-16 para IO-bound. Las pruebas prácticas muestran que para una aplicación móvil típica, 3-4 hilos son óptimos: más hilos aumentan el consumo de energía sin mejorar el rendimiento.
El tamaño de la cola de tareas (work queue) determina cuántas tareas pueden esperar para ejecutarse. Cola ilimitada (LinkedBlockingQueue sin límite) puede provocar OOM con una llegada rápida de tareas. Cola limitada (ArrayBlockingQueue con tamaño fijo) rechaza tareas cuando está llena. Para aplicaciones móviles, se recomienda ArrayBlockingQueue con una capacidad de 16-32 tareas. CallerRunsPolicy es el mejor RejectedExecutionHandler para móviles: ralentiza al llamante (contrapresión) en lugar de perder la tarea.
| Parámetro | Recomendación para móviles | Justificación |
|---|---|---|
| corePoolSize | 2-4 | Recursos limitados del dispositivo móvil |
| maxPoolSize | corePoolSize (o +1-2) | Evitar picos de carga por creación de hilos |
| keepAliveTime | 15-30 segundos | Liberación rápida de memoria sin creación frecuente |
| Capacidad de cola | 16-32 | Equilibrio entre almacenamiento en búfer y riesgo de OOM |
| Handler | CallerRunsPolicy | Contrapresión sin pérdida de tareas |
Los desarrolladores de aplicaciones móviles suelen cometer errores al usar el thread pool que provocan fallos, fugas de memoria y funcionamiento inestable. Los más comunes: no llamar a shutdown() en ExecutorService, crear un nuevo pool para cada operación, un pool demasiado grande, deadlock entre tareas, usar CachedThreadPool en Android.
El deadlock ocurre cuando una tarea en el pool espera el resultado de otra tarea del mismo pool, pero todos los hilos están ocupados esperando. Ejemplo: la tarea A envía la tarea B al mismo pool y llama a future.get() — si el pool está agotado, la tarea A espera a la tarea B, y la tarea B no puede ejecutarse porque no hay hilos libres. Solución: use pools separados para diferentes niveles de tareas o callbacks asíncronos en lugar de .get() bloqueante.
// Deadlock en Thread Pool
val pool = Executors.newFixedThreadPool(1)
// ¡La tarea A espera a la tarea B — deadlock!
val futureA = pool.submit {
// Esta tarea nunca se ejecutará
val futureB = pool.submit { 42 }
futureB.get() // Se bloquea para siempre
}
// Solución: pools separados
val workerPool = Executors.newFixedThreadPool(2)
val callbackPool = Executors.newSingleThreadExecutor()
workerPool.submit {
callbackPool.submit {
// Se ejecuta en un pool separado — deadlock imposible
}
}
// O use CompletableFuture
workerPool.submit {
CompletableFuture
.supplyAsync { 42 }
.thenAccept { result ->
println(result)
}
}
Un ExecutorService creado en una Activity debe finalizarse en onDestroy(). Si no se hace, los hilos permanecerán en memoria incluso después de destruir la Activity. Solución: mantenga el pool en el ámbito de Application o ViewModel, no en Activity. Para corrutinas, use viewModelScope o lifecycleScope. Si el pool se crea dentro de una Activity, asegúrese de llamar a pool.shutdown() en onDestroy(). Para pruebas, use es.shutdownNow() para una detención inmediata.
Preguntas frecuentes
Thread Pool reutiliza hilos ya creados para ejecutar múltiples tareas. Un hilo normal (raw Thread) se crea, ejecuta una tarea y se destruye. Crear un hilo consume aproximadamente 1 MB de memoria y ~1 ms de tiempo. Thread Pool reduce los costos adicionales, limita la cantidad máxima de hilos y proporciona una API para la gestión (shutdown, awaitTermination).
Para una aplicación móvil típica, lo óptimo son 2-4 hilos. Para tareas CPU-bound — el número de núcleos de CPU. Para tareas IO-bound — número de núcleos × 2. Más hilos aumentan el consumo de energía y el cambio de contexto sin mejorar el rendimiento. En Android, use Process.availableProcessors() para determinar los núcleos. En iOS — ProcessInfo.processInfo.processorCount.
CachedThreadPool crea hilos según sea necesario y reutiliza los existentes. El problema: no limita la cantidad máxima de hilos. Si llegan 100 tareas simultáneamente, se crearán 100 hilos. Esto provoca OOM en Android (cada hilo ~1 MB). Use newFixedThreadPool(n) con un límite explícito. CachedThreadPool solo es aceptable para tareas rápidas con un volumen pequeño garantizado.
Sí, si el pool no pertenece a un contenedor gestionado (como las corrutinas). shutdown() detiene la recepción de nuevas tareas y finaliza los hilos después de completar las actuales. Sin shutdown(), los hilos permanecen en memoria y la aplicación no termina. Para Activity, llámelo en onDestroy(). Para ViewModel, use coroutineScope. Finalizar el pool es parte obligatoria de la gestión de recursos, similar a cerrar un Cursor o InputStream.
Sí, OperationQueue y DispatchQueue son pools de hilos proporcionados por iOS. OperationQueue limita la concurrencia a través de maxConcurrentOperationCount. DispatchQueue.global() utiliza el pool de hilos del sistema sin control directo. A diferencia de Java ThreadPoolExecutor, no se gestiona corePoolSize ni la capacidad de la cola: el sistema optimiza el pool automáticamente según la carga actual y el consumo de energía del dispositivo.
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