Thread — unità base del tempo processore, con un proprio stack e che si esegue indipendentemente dagli altri thread. Nello sviluppo mobile, i thread vengono utilizzati per l'esecuzione parallela dei task, in modo che l'interfaccia rimanga reattiva durante le operazioni lunghe. Android supporta java.lang.Thread, Executors e Kotlin Coroutines, iOS — Thread (Objective-C), GCD e OperationQueue. Secondo la Documentazione Android Thread, la creazione di un thread nativo richiede l'allocazione di circa 1 MB di stack da parte del sistema operativo.
Punti chiave
Thread (filo di esecuzione) — è una sequenza indipendente di istruzioni che il sistema operativo può schedulare su un core della CPU. Ogni processo (applicazione) contiene almeno un thread — il Main Thread. Thread aggiuntivi vengono creati per l'esecuzione parallela dei task. Ogni thread ha il proprio stack software (con variabili locali), un contatore di programma (PC) e registri. La memoria heap è condivisa da tutti i thread del processo.
Nei sistemi operativi mobili, i thread vengono schedulati con multitasking preemptive: l'OS può interrompere l'esecuzione di un thread in qualsiasi momento e passare il controllo a un altro (context switch). Il cambio di contesto è un'operazione costosa (1-10 microsecondi), poiché richiede il salvataggio/ripristino dei registri della CPU, l'aggiornamento del TLB e lo svuotamento delle cache. È per questo che un numero eccessivo di thread (centinaia o migliaia) degrada le prestazioni — l'OS spende più tempo nei cambi di contesto che nell'esecuzione.
Thread e processo — sono concetti diversi. Un processo è un'istanza dell'applicazione con memoria virtuale dedicata. Un thread all'interno di un processo condivide questa memoria con altri thread. In Android, ogni componente dell'applicazione (Activity, Service, BroadcastReceiver) opera in un singolo processo, ma può eseguirsi su thread diversi. Un'applicazione iOS è anch'essa un singolo processo con la possibilità di creare thread aggiuntivi tramite GCD o Thread.
Ogni thread in Java/Kotlin (Android) e NSThread (iOS) attraversa cinque stati: New (creato), Runnable (pronto per l'esecuzione), Running (in esecuzione sulla CPU), Blocked/Waiting (attende una risorsa o una notifica), Terminated (terminato). Le transizioni tra gli stati sono gestite dallo scheduler dell'OS e dalle primitive di sincronizzazione. Lo sviluppatore può influenzare la priorità del thread (Thread.setPriority()) e il suo stato (sleep, join, interrupt).
In Android, un thread passa allo stato Blocked quando tenta di acquisire un monitor occupato (synchronized), chiama Object.wait() o Thread.sleep(). In iOS — quando chiama NSCondition.wait(), pthread_cond_wait() o dispatch_semaphore_wait(). Nello stato Blocked, il thread non consuma CPU ma occupa memoria (stack). Un thread può essere interrotto (interrupted) da un altro thread, ricevendo InterruptedException (Java) o verificando isCancelled (Kotlin Coroutines).
| Stato | Descrizione | Metodo di transizione |
|---|---|---|
| New | Thread creato ma non avviato | Costruttore Thread() |
| Runnable | Thread pronto per l'esecuzione, attende la CPU | thread.start() |
| Running | Thread in esecuzione su un core CPU | Scheduler dell'OS |
| Blocked/Waiting | Thread attende una risorsa, un monitor o una notifica | synchronized, wait(), sleep() |
| Terminated | Thread ha terminato run() o è stato interrotto | run() terminato, interrupt() |
Context switch (cambio di contesto) — operazione in cui l'OS salva lo stato del thread corrente (registri, PC, TLB) e carica lo stato salvato di un altro. Nei sistemi mobili (Linux + ART, XNU per iOS), un context switch richiede 1-10 microsecondi. Se un thread esegue un task in 100 microsecondi e il context switch ne richiede 5, allora il 5% del tempo viene sprecato. Per minimizzare i context switch, iOS utilizza GCD con work stealing, Android — pool con fixedThreadCount.
Android si è evoluto dal java.lang.Thread di basso livello alle moderne coroutine. Ogni livello di astrazione offre più possibilità con meno overhead. Thread è la classe base, ma la sua creazione diretta non è raccomandata: un nuovo thread non è gestito da un pool, è difficile da monitorare e annullare. AsyncTask (deprecato dall'API 30) era un passo avanti, ma soffriva di perdite di memoria e gestione scomoda delle configurazioni.
HandlerThread è una sottoclasse speciale di Thread con Looper, che può elaborare una coda di messaggi. Viene utilizzato per l'esecuzione sequenziale di task su un thread in background, ad esempio per scrivere dati in Room o file. HandlerThread viene creato chiamando start(), dopodiché tramite Handler(handlerThread.looper) è possibile inviare messaggi e Runnable. La chiamata handlerThread.quit() arresta il Looper e termina il thread.
// Android: Thread, HandlerThread e Executors
import android.os.Handler
import android.os.HandlerThread
import java.util.concurrent.Executors
class ThreadExample {
// 1. Creazione diretta di Thread (non consigliata)
fun directThread() {
val thread = Thread(Runnable {
Thread.sleep(1000)
print("Direct thread executed")
})
thread.start()
}
// 2. HandlerThread per task sequenziali in background
fun handlerThreadExample() {
val handlerThread = HandlerThread("BackgroundQueue")
handlerThread.start()
val handler = Handler(handlerThread.looper)
handler.post {
// Esecuzione sequenziale su thread in background
Thread.sleep(500)
print("HandlerThread: task completato")
}
// Arresto del thread (eseguito quando i task sono completati)
handlerThread.quitSafely()
}
// 3. Executors — pool di thread
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 — lo standard moderno
suspend fun coroutineExample() = kotlinx.coroutines.withContext(
kotlinx.coroutines.Dispatchers.Default
) {
print("Coroutine on thread: ${Thread.currentThread().getName()}")
}
}
L'esempio ThreadExample mostra tutti e quattro i livelli di astrazione dei thread in Android. La creazione diretta di Thread è l'approccio più basso livello e meno efficiente. HandlerThread è utile per task sequenziali in background. Executors.newFixedThreadPool(4) crea un pool di 4 thread per l'esecuzione parallela di fino a 10 task. Kotlin Coroutines con Dispatchers.Default — un metodo moderno, efficiente e sicuro.
HandlerThread — una sottoclasse specializzata di Thread con Looper integrato e coda di messaggi. Viene creato chiamando start(), dopodiché tramite Handler(handlerThread.looper) è possibile inviare Runnable e messaggi. HandlerThread esegue i task rigorosamente in sequenza — il task successivo non inizia prima del completamento del precedente. È utile per scrivere dati in Room o file, dove l'ordine delle operazioni è critico. La chiamata quitSafely() arresta il Looper dopo il completamento del task corrente.
iOS offre anch'esso tre livelli di lavoro con i thread. Thread (Thread in Swift, NSThread in Objective-C) — un'API di basso livello che crea direttamente un thread nativo. GCD (Grand Central Dispatch) tramite DispatchQueue — lo strumento principale per gli sviluppatori iOS, che gestisce automaticamente il pool di thread. OperationQueue — un'astrazione di alto livello su GCD con supporto per dipendenze, priorità e annullamento.
L'uso diretto di Thread nello sviluppo iOS moderno è estremamente raro — GCD fornisce tutte le funzionalità necessarie con gestione automatica della memoria e dei thread. Thread viene utilizzato solo per casi specifici: impostazione di thread-local storage (threadDictionary), creazione di un RunLoop per un thread in background o integrazione con librerie C che si aspettano pthread_t.
import Foundation
class ThreadManager {
// 1. Thread (basso livello)
func createThread() {
let thread = Thread {
// Il codice viene eseguito su un nuovo thread
print("Current thread: \(Thread.current)")
}
thread.name = "com.app.worker"
thread.qualityOfService = .utility
thread.start()
}
// 2. GCD — DispatchQueue
func gcdExample() {
// Coda parallela
let queue = DispatchQueue(label: "com.app.concurrent",
qos: .utility,
attributes: .concurrent)
queue.async {
print("GCD async task")
}
// Barrier per la sincronizzazione della scrittura
queue.async(flags: .barrier) {
// Accesso esclusivo durante la scrittura
print("Barrier write: exclusive access")
}
}
// 3. OperationQueue con dipendenze
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...")
}
// Dipendenze: download -> process -> save
process.addDependency(download)
save.addDependency(process)
queue.addOperations([download, process, save], waitUntilFinished: false)
}
}
// Collezione thread-safe tramite GCD barrier
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 classe ThreadSafeArray dimostra il pattern Lettura Concorrente / Scrittura Esclusiva tramite GCD barrier. La lettura tramite queue.sync{} viene eseguita in parallelo da più thread. La scrittura tramite queue.async(flags: .barrier) blocca tutte le altre operazioni (sia lettura che scrittura) fino al completamento della scrittura. È più efficiente dei blocchi synchronized perché non blocca i lettori quando non c'è scrittura.
L'uso diretto di Thread in iOS è giustificato in tre casi: per thread-local storage (Thread.current.threadDictionary) — conservazione di dati legati al thread; per la creazione di un RunLoop speciale su un thread in background con performSelector:onThread:; per l'integrazione con librerie C/C++ che si aspettano pthread_t. In tutti gli altri casi, GCD tramite DispatchQueue è preferibile — gestisce automaticamente il pool di thread e il consumo energetico.
Race condition (condizione di competizione) si verifica quando due o più thread accedono simultaneamente a dati condivisi e almeno uno dei thread esegue una scrittura. Il risultato dipende dall'ordine di esecuzione (timing) ed è imprevedibile. Per prevenire le race condition, si utilizzano primitive di sincronizzazione. Nello sviluppo mobile sono disponibili blocchi (synchronized, NSLock), operazioni atomiche (AtomicInteger, proprietà atomic di iOS) e code (serial queue).
La scelta della primitiva dipende dallo scenario. Per contatori semplici e flag, le operazioni atomiche sono sufficienti (AtomicInteger, proprietà atomic). Per sezioni critiche con più operazioni — blocchi (synchronized, NSLock). Per strutture dati complesse — una serial DispatchQueue o GCD barrier. I blocchi sono più facili da capire, ma sono soggetti a deadlock e livelock. Le code sono più complesse ma più sicure.
// Sincronizzazione in Android/Kotlin
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class Counter {
// 1. AtomicInteger — per contatori semplici
private val atomicCount = AtomicInteger(0)
fun incrementAtomic() = atomicCount.incrementAndGet()
// 2. synchronized — per sezioni critiche
@Synchronized
fun synchronizedOperation() {
// Un solo thread alla volta
doWork()
}
// 3. Mutex dalle coroutine — suspend-safe
private val mutex = Mutex()
suspend fun mutexOperation() {
mutex.withLock {
// protected code — thread-safe
doWork()
}
}
private fun doWork() { /* critical section */ }
}
// Esempio di deadlock: A blocca B, B blocca 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 illustra tre approcci alla sincronizzazione. AtomicInteger.incrementAndGet() — operazione atomica senza blocchi (CAS). @Synchronized — monitor integrato di Java, blocca l'intero oggetto. Mutex.withLock — mutex delle coroutine, sospende la coroutine invece di bloccare il thread (più efficiente). DeadlockExample mostra un deadlock classico: due thread acquisiscono blocchi in ordine diverso.
Thread Pool (pool di thread) — un insieme di thread pre-creati che vengono riutilizzati per eseguire task. Invece di creare un nuovo thread per ogni task (costoso), il pool prende un thread libero dal pool. Se non ci sono thread liberi, il task viene messo in coda. Il pool gestisce automaticamente la dimensione: nuovi thread vengono creati durante i picchi di carico, i thread inattivi vengono terminati. Ciò riduce l'overhead di creazione dei thread di decine di volte.
In Android, Executors.newFixedThreadPool(4) crea un pool di 4 thread. Se arrivano contemporaneamente 10 task, 4 iniziano immediatamente, 6 aspettano in coda. Executors.newCachedThreadPool() crea thread secondo necessità (senza limiti) e termina quelli inattivi dopo 60 secondi. Per iOS, GCD fornisce automaticamente pool di code globali, la cui dimensione corrisponde al numero di core CPU e al carico corrente.
In Kotlin Coroutines, i pool di thread sono nascosti all'interno dei dispatcher. Dispatchers.Default utilizza un pool di dimensione pari al numero di core CPU (minimo 2). Dispatchers.IO — 64 thread (sufficienti per centinaia di task IO-bound, poiché la maggior parte attenderà I/O senza occupare la CPU). Ogni dispatcher adatta automaticamente la dimensione del pool al carico, risparmiando energia della batteria a riposo.
Domande frequenti
Thread — unità base di esecuzione del codice in un'applicazione. Ogni processo può avere molti thread che condividono la memoria ma con il proprio stack. Nello sviluppo mobile, i thread vengono utilizzati per l'esecuzione parallela dei task senza bloccare l'UI. Android utilizza Thread, Executors, HandlerThread e Coroutines. iOS utilizza Thread, GCD (DispatchQueue) e OperationQueue.
La creazione di Thread richiede l'allocazione di circa 1 MB di stack in Android e ~512 KB in iOS — è un'operazione costosa. Per 1000 task, la creazione diretta di 1000 thread richiederebbe circa 1 GB solo per gli stack, più l'overhead del context switch. Invece di Thread, utilizzate pool (Executors, GCD) o coroutine — riutilizzano i thread, riducendo l'overhead di decine di volte.
Race condition — comportamento imprevedibile quando più thread accedono simultaneamente a dati condivisi con scrittura. Si può evitare in tre modi: utilizzare tipi atomici (AtomicInteger), blocchi (synchronized, NSLock) o serializzare l'accesso tramite coda (DispatchQueue serial, Actor in Kotlin). La best practice è minimizzare lo stato mutable condiviso e utilizzare l'immutabilità.
Thread — un oggetto di sistema nativo, che occupa circa 1 MB di stack ed è legato a un core dell'OS. Coroutine — un'unità di esecuzione leggera di Kotlin, non legata a un thread specifico e che può sospendersi (suspend) senza bloccare. Un singolo thread può eseguire migliaia di coroutine. Le coroutine sono più efficienti in memoria e permettono di scrivere codice asincrono senza callback.
Deadlock si manifesta come un blocco completo dell'applicazione senza ANR. In Android, utilizzate Thread.getAllStackTraces() per ottenere gli stack di tutti i thread — due thread attenderanno i blocchi l'uno dell'altro. In iOS — Thread.callStackSymbols. Strumenti: Android Studio Profiler (scheda Threads), Instruments (iOS, Thread State View). Prevenzione: acquisite i blocchi in un ordine fisso, utilizzate tryLock con timeout.
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