Thread Pool — è un meccanismo di gestione dei thread in cui un pool di thread pre-creato viene riutilizzato per eseguire attività, evitando il sovraccarico di creazione e distruzione dei thread. Nello sviluppo mobile, il thread pool viene utilizzato per operazioni in background: richieste di rete, elaborazione di immagini, lavoro con database. Secondo la documentazione Google Android (2025), ExecutorService è il metodo consigliato per gestire i thread in background in Android. In iOS, OperationQueue e GCD DispatchQueue con code concurrent globali svolgono un ruolo simile.
Punti chiave
Thread Pool (pool di thread) — è un pattern architetturale in cui un numero fisso di thread viene creato in anticipo e riutilizzato per eseguire più attività. Invece di creare un nuovo thread per ogni operazione (che è costoso: circa 1 MB di stack per thread nella JVM), le attività vengono inserite in una coda ed eseguite dai thread disponibili del pool. Nello sviluppo mobile, il thread pool è critico per le prestazioni — Android e iOS limitano il numero di thread per applicazione.
Creare un thread è un'operazione costosa: allocazione dello stack, registrazione nel sistema, cambio di contesto. Sui dispositivi mobili con risorse limitate, la creazione incontrollata di thread porta a OOM (OutOfMemoryError) su Android e throttling su iOS. Thread Pool risolve entrambi i problemi: limita il numero massimo di thread in esecuzione simultanea e riutilizza i thread già creati. Google raccomanda ExecutorService invece di raw Thread(), Apple raccomanda OperationQueue invece di Thread.
| Parametro | Senza pool (raw Thread) | Con Thread Pool |
|---|---|---|
| Creazione thread | Per ogni attività | Una volta alla creazione del pool |
| Massimo thread | Illimitato (rischio OOM) | Limitato da core/max pool size |
| Utilizzo | Basso (il thread muore dopo l'attività) | Alto (il thread viene riutilizzato) |
| Gestione | Manuale (join, interrupt) | Automatica (ExecutorService) |
| Consumo memoria | Cresce con ogni attività | Fisso |
Il thread pool funziona secondo il principio Produttore-Consumatore: le attività (Runnable/Callable) vengono inserite in una coda bloccante (BlockingQueue). I thread del pool attendono le attività nella coda e le prelevano per l'esecuzione. Algoritmo: se il numero di thread liberi è inferiore a corePoolSize, viene creato un nuovo thread. Se corePoolSize viene raggiunto, l'attività viene inserita nella coda. Se la coda è piena e il numero di thread è inferiore a maximumPoolSize, viene creato un thread aggiuntivo. Se maximumPoolSize viene superato, l'attività viene rifiutata tramite RejectedExecutionHandler.
Core pool size — il numero di thread che vengono mantenuti nel pool anche quando inattivi. Maximum pool size — il numero massimo di thread che possono essere creati quando la coda trabocca. La differenza tra loro sono i thread aggiuntivi (overflow) che vengono creati temporaneamente e terminati dopo il timeout di inattività. Sui dispositivi mobili, si consiglia di impostare corePoolSize uguale a maximumPoolSize per evitare picchi di carico dalla creazione di thread.
BlockingQueue memorizza le attività in attesa di esecuzione. Le implementazioni più popolari: LinkedBlockingQueue (illimitata), ArrayBlockingQueue (limitata) e SynchronousQueue (senza memorizzazione — l'attività viene passata direttamente a un thread). Quando la coda e il pool sono pieni, viene attivato RejectedExecutionHandler. Le politiche standard sono: AbortPolicy (lancia RejectedExecutionException), CallerRunsPolicy (esegue nel thread del chiamante), DiscardPolicy e DiscardOldestPolicy.
// Creazione di Thread Pool in Android
val threadPool = ThreadPoolExecutor(
corePoolSize = 2, // Minimo 2 thread
maximumPoolSize = 4, // Massimo 4 thread
keepAliveTime = 30L, // Durata del thread di overflow
unit = TimeUnit.SECONDS,
workQueue = LinkedBlockingQueue<Runnable>(16),
threadFactory = Executors.defaultThreadFactory(),
handler = ThreadPoolExecutor.CallerRunsPolicy()
)
// Invio di attività
threadPool.execute {
val result = api.fetchData()
runOnUiThread { showData(result) }
}
// Arresto del pool
threadPool.shutdown()
// Attesa del completamento di tutte le attività
threadPool.awaitTermination(10, TimeUnit.SECONDS)
Android fornisce diverse implementazioni di thread pool tramite java.util.concurrent. Executors — una factory con configurazioni pronte: newFixedThreadPool(n) (pool fisso), newCachedThreadPool() (illimitato, thread creati secondo necessità), newSingleThreadExecutor() (thread singolo — esecuzione sequenziale). Per i progetti mobili, si consiglia newFixedThreadPool con un limite ragionevole (2-4 thread), poiché il pool cached può creare troppi thread.
ThreadPoolExecutor (TPE) — un'implementazione completa di ExecutorService con parametri configurabili. Su Android, TPE viene utilizzato all'interno di AsyncTask, IntentService e JobIntentService. I parametri corePoolSize, maximumPoolSize, keepAliveTime, BlockingQueue e RejectedExecutionHandler consentono di ottimizzare il comportamento del pool. Raccomandazioni per Android: corePoolSize = numero di core CPU - 1 (per attività IO-bound) o numero di core (per attività CPU-bound). Per applicazioni tipiche: 2-4 thread.
// Configurazioni pronte di Executors
// 1. Pool fisso di 3 thread
val fixedPool = Executors.newFixedThreadPool(3)
// 2. Pool cached (non raccomandato per mobile)
val cachedPool = Executors.newCachedThreadPool()
// 3. Thread singolo (serializzazione)
val singlePool = Executors.newSingleThreadExecutor()
// 4. Pianificatore (attività periodiche)
val scheduler = Executors.newScheduledThreadPool(2)
// Utilizzo con Callable e Future
val future: Future<String> = fixedPool.submit(Callable {
"Result: ${api.call()}"
})
// Ottenimento del risultato (blocca il thread)
val result = future.get(5, TimeUnit.SECONDS)
// Arresto del pool
fixedPool.shutdownNow()
Le coroutine Kotlin forniscono CoroutineDispatcher — un'astrazione simile al thread pool. Dispatchers.IO utilizza un pool di 64 thread (limitato). Dispatchers.Default — un pool pari al numero di core CPU. CoroutineDispatcher non richiede arresto manuale e viene gestito automaticamente. Per un'ottimizzazione precisa, crea un ExecutorCoroutineDispatcher personalizzato tramite Executors.newFixedThreadPool(2).asCoroutineDispatcher(). Le coroutine non sostituiscono il thread pool, ma lo incapsulano.
iOS fornisce due meccanismi principali per gestire il thread pool: OperationQueue (API di alto livello basata su GCD) e GCD DispatchQueue (API di basso livello in C). OperationQueue incapsula un thread pool tramite la proprietà maxConcurrentOperationCount. Per impostazione predefinita, OperationQueue utilizza un massimo definito dal sistema (dipende dal carico del sistema). DispatchQueue.global() fornisce una coda concurrent con un pool di thread di sistema.
OperationQueue gestisce il pool di thread tramite maxConcurrentOperationCount. Il valore 1 crea una coda seriale (analoga a un pool a thread singolo). Un valore maggiore di 1 crea un pool concurrent con il limite specificato. Per impostazione predefinita, maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount (ottimo di sistema, generalmente 4-8 thread). Operation supporta dipendenze, priorità e annullamento. Ogni operazione viene eseguita su qualsiasi thread disponibile del pool di sistema.
// OperationQueue con pool di 3 thread
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
queue.qualityOfService = .utility
// Creazione di operazioni
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) }
}
// Dipendenza: operation2 attende operation1
operation2.addDependency(operation1)
// Aggiunta alla coda
queue.addOperations([operation1, operation2], waitUntilFinished: false)
// Annullamento di tutte le operazioni
queue.cancelAllOperations()
DispatchQueue — è il pool di thread di Apple. Una coda concurrent (qos: .utility) utilizza il pool di thread di sistema, ottimizzato per il carico attuale del dispositivo. Diversi livelli QoS (userInteractive, userInitiated, utility, background) corrispondono a diversi pool con diverse priorità. DispatchGroup consente di sincronizzare più attività. DispatchWorkItem supporta l'annullamento e qualityOfService. Per un controllo preciso, crea code concurrent personalizzate tramite DispatchQueue(label: qos: attributes: .concurrent).
// GCD DispatchQueue come thread pool
let customQueue = DispatchQueue(
label: "com.app.background",
qos: .utility,
attributes: .concurrent,
autoreleaseFrequency: .workItem
)
// Invio di attività al pool
customQueue.async { self.processFile(file1) }
customQueue.async { self.processFile(file2) }
// DispatchGroup per sincronizzazione
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() // Entrambe le attività completate
}
// Limitazione della concorrenza tramite semaforo
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
pool.async {
semaphore.wait()
download(url)
semaphore.signal()
}
}
La configurazione del thread pool influisce direttamente sulle prestazioni dell'applicazione. Parametri errati portano a sottoutilizzo della CPU (troppi pochi thread) o sovraccarico del sistema (troppi). Per le applicazioni mobili, i valori ottimali differiscono da quelli lato server a causa delle risorse limitate e del consumo energetico. I parametri principali sono: corePoolSize, maxPoolSize, capacità della coda e keepAliveTime.
Formula per attività IO-bound: corePoolSize = numero di core CPU × 2 (i thread attendono I/O). Per attività CPU-bound: corePoolSize = numero di core CPU (i thread sono costantemente occupati con calcoli). Sui dispositivi mobili moderni (6-8 core), questo dà 6-8 thread per CPU-bound e 12-16 per IO-bound. Test pratici mostrano che per un'applicazione mobile tipica, 3-4 thread sono ottimali — più thread aumentano il consumo energetico senza migliorare le prestazioni.
La dimensione della coda di lavoro (work queue) determina quante attività possono attendere l'esecuzione. Coda illimitata (LinkedBlockingQueue senza limite) può portare a OOM con arrivo rapido di attività. Coda limitata (ArrayBlockingQueue con dimensione fissa) rifiuta le attività quando è piena. Per le applicazioni mobili, si consiglia ArrayBlockingQueue con una capacità di 16-32 attività. CallerRunsPolicy è il miglior RejectedExecutionHandler per dispositivi mobili: rallenta il chiamante (backpressure) invece di perdere l'attività.
| Parametro | Raccomandazione per mobile | Motivazione |
|---|---|---|
| corePoolSize | 2-4 | Risorse limitate del dispositivo mobile |
| maxPoolSize | corePoolSize (o +1-2) | Evitare picchi di carico dalla creazione di thread |
| keepAliveTime | 15-30 secondi | Rilascio rapido memoria senza creazione frequente |
| Capacità coda | 16-32 | Equilibrio tra buffering e rischio OOM |
| Handler | CallerRunsPolicy | Backpressure senza perdita di attività |
Gli sviluppatori di applicazioni mobili commettono spesso errori nell'uso del thread pool che portano a crash, perdite di memoria e funzionamento instabile. Più comuni: non chiamare shutdown() per ExecutorService, creare un nuovo pool per ogni operazione, pool troppo grande, deadlock tra attività, usare CachedThreadPool su Android.
Il deadlock si verifica quando un'attività nel pool attende il risultato di un'altra attività dello stesso pool, ma tutti i thread sono occupati ad attendere. Esempio: l'attività A invia l'attività B allo stesso pool e chiama future.get() — se il pool è esaurito, l'attività A attende l'attività B, e l'attività B non può eseguire perché non ci sono thread liberi. Soluzione: utilizzare pool separati per diversi livelli di attività o callback asincroni invece di .get() bloccante.
// Deadlock in Thread Pool
val pool = Executors.newFixedThreadPool(1)
// L'attività A attende l'attività B — deadlock!
val futureA = pool.submit {
// Questa attività non verrà mai eseguita
val futureB = pool.submit { 42 }
futureB.get() // Si blocca per sempre
}
// Correzione: pool separati
val workerPool = Executors.newFixedThreadPool(2)
val callbackPool = Executors.newSingleThreadExecutor()
workerPool.submit {
callbackPool.submit {
// Esecuzione in pool separato — deadlock impossibile
}
}
// Oppure usa CompletableFuture
workerPool.submit {
CompletableFuture
.supplyAsync { 42 }
.thenAccept { result ->
println(result)
}
}
Un ExecutorService creato in un'Activity deve essere arrestato in onDestroy(). Se non lo si fa, i thread rimarranno in memoria anche dopo la distruzione dell'Activity. Soluzione: mantenere il pool nell'ambito dell'Application o del ViewModel, non nell'Activity. Per le coroutine, utilizzare viewModelScope o lifecycleScope. Se il pool viene creato all'interno di un'Activity, assicurati di chiamare pool.shutdown() in onDestroy(). Per i test, utilizzare es.shutdownNow() per un arresto immediato.
Domande frequenti
Thread Pool riutilizza thread già creati per eseguire più attività. Un thread normale (raw Thread) viene creato, esegue un'attività e viene distrutto. La creazione di un thread richiede circa 1 MB di memoria e ~1 ms di tempo. Thread Pool riduce il sovraccarico, limita il numero massimo di thread e fornisce un'API per la gestione (shutdown, awaitTermination).
Per un'applicazione mobile tipica, 2-4 thread sono ottimali. Per attività CPU-bound — il numero di core CPU. Per attività IO-bound — numero di core × 2. Più thread aumentano il consumo energetico e il cambio di contesto senza migliorare le prestazioni. Su Android, utilizzare Process.availableProcessors() per determinare i core. Su iOS — ProcessInfo.processInfo.processorCount.
CachedThreadPool crea thread secondo necessità e riutilizza quelli esistenti. Il problema: non limita il numero massimo di thread. Se 100 attività arrivano simultaneamente, verranno creati 100 thread. Questo porta a OOM su Android (ogni thread ~1 MB). Utilizzare newFixedThreadPool(n) con un limite esplicito. CachedThreadPool è accettabile solo per attività burst di breve durata con volume ridotto garantito.
Sì, se il pool non appartiene a un contenitore gestito (come le coroutine). shutdown() interrompe l'accettazione di nuove attività e termina i thread dopo il completamento di quelle correnti. Senza shutdown(), i thread rimangono in memoria e l'applicazione non termina. Per l'Activity, chiamalo in onDestroy(). Per il ViewModel, utilizza coroutineScope. L'arresto del pool è una parte obbligatoria della gestione delle risorse, simile alla chiusura di un Cursor o InputStream.
Sì, OperationQueue e DispatchQueue sono pool di thread forniti da iOS. OperationQueue limita la concorrenza tramite maxConcurrentOperationCount. DispatchQueue.global() utilizza il pool di thread di sistema senza controllo diretto. A differenza di Java ThreadPoolExecutor, non gestisci corePoolSize o capacità della coda — il sistema ottimizza il pool automaticamente in base al carico corrente e al consumo energetico del dispositivo.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche