Thread Pool nello sviluppo mobile — basi, pool di thread e principio di funzionamento

Autore: IT Sectr Pubblicato: 2026-03-18 Tempo di lettura: 11 min

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 — un pool di thread riutilizzabili per eseguire attività in background senza il sovraccarico di creazione dei thread.
  • ExecutorService in Android gestisce il pool tramite ThreadPoolExecutor con parametri configurabili.
  • OperationQueue in iOS incapsula un thread pool tramite maxConcurrentOperationCount.
  • Core pool size — il numero minimo di thread sempre pronti a eseguire attività.
  • Work queue memorizza le attività in attesa di un thread libero nel pool.

Cos'è Thread Pool?

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.

Perché Thread Pool è importante nello sviluppo mobile

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.

ParametroSenza pool (raw Thread)Con Thread Pool
Creazione threadPer ogni attivitàUna volta alla creazione del pool
Massimo threadIllimitato (rischio OOM)Limitato da core/max pool size
UtilizzoBasso (il thread muore dopo l'attività)Alto (il thread viene riutilizzato)
GestioneManuale (join, interrupt)Automatica (ExecutorService)
Consumo memoriaCresce con ogni attivitàFisso

Come funziona Thread Pool nello sviluppo mobile?

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 vs Maximum Pool Size

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.

Work Queue e RejectedExecutionHandler

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.

kotlin
// 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)

Thread Pool in Android: ExecutorService

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 in Android

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.

kotlin
// 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()

CoroutineDispatcher come thread pool

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.

Thread Pool in iOS: OperationQueue e GCD

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 e maxConcurrentOperationCount

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.

swift
// 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()

GCD DispatchQueue come thread pool

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).

swift
// 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()
    }
}

Parametri di configurazione di Thread Pool

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.

Calcolo della dimensione ottimale del pool

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.

Capacità della coda e comportamento di overflow

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à.

ParametroRaccomandazione per mobileMotivazione
corePoolSize2-4Risorse limitate del dispositivo mobile
maxPoolSizecorePoolSize (o +1-2)Evitare picchi di carico dalla creazione di thread
keepAliveTime15-30 secondiRilascio rapido memoria senza creazione frequente
Capacità coda16-32Equilibrio tra buffering e rischio OOM
HandlerCallerRunsPolicyBackpressure senza perdita di attività

Errori comuni nell'uso del pool di thread

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.

Deadlock in Thread Pool

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.

kotlin
// 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)
        }
}

Pool non terminato e perdite

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

In cosa differisce Thread Pool da un thread normale?

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).

Quanti thread dovrebbe avere un pool per un'applicazione mobile?

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.

Cos'è CachedThreadPool e perché è pericoloso su Android?

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.

È necessario chiamare shutdown() per ExecutorService?

, 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.

OperationQueue e DispatchQueue sono pool di thread?

, 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

  • Thread Pool — un pool di thread riutilizzabili per attività in background, riducendo il sovraccarico di creazione dei thread.
  • Android utilizza ThreadPoolExecutor e Executors.newFixedThreadPool(n) con limite esplicito della dimensione del pool.
  • iOS fornisce OperationQueue con maxConcurrentOperationCount e GCD DispatchQueue con pool QoS.
  • Core pool size — il numero minimo di thread; maximum pool size — il massimo quando la coda trabocca.
  • Deadlock nel pool si verifica quando un'attività si blocca in attesa di un'altra attività dello stesso pool.
  • CallerRunsPolicy è preferibile per le applicazioni mobili — rallenta il chiamante senza perdere attività.
  • Per un'applicazione mobile tipica, la dimensione ottimale del pool è di 2-4 thread con shutdown() per la pulizia.

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.

Discuti il progetto

Leggi anche