Dispatchers in Kotlin Coroutines sono componenti di CoroutineContext che determinano i thread per l'esecuzione delle coroutine: Main (thread UI), IO (rete e disco), Default (compiti intensivi di CPU) e Unconfined (thread corrente). Ogni dispatcher gestisce un pool di thread specializzato ottimizzato per un tipo specifico di lavoro. Secondo la guida JetBrains, 2024, scegliere il dispatcher giusto è fondamentale per le prestazioni e la stabilità dell'applicazione.
Punti Chiave
Dispatchers sono implementazioni dell'interfaccia CoroutineDispatcher, che sono elementi di CoroutineContext. Determinano su quale thread o pool di thread verrà eseguita la coroutine. Quando si crea una coroutine tramite launch o async, il dispatcher può essere passato come primo parametro: launch(Dispatchers.IO) { ... }. Se non viene specificato alcun dispatcher, viene ereditato dal CoroutineScope esterno.
Kotlin fornisce quattro dispatcher incorporati: Main, IO, Default, Unconfined. Ogni dispatcher utilizza il proprio pool di thread ottimizzato per un tipo specifico di operazione. Scegliere il dispatcher giusto determina le prestazioni dell'applicazione: una scelta errata porta a rallentamenti dell'interfaccia, core CPU inattivi o uso inefficiente dei thread.
| Dispatcher | Pool di thread | Max thread | Utilizzo |
|---|---|---|---|
| Dispatchers.Main | Uno (UI) | 1 | Aggiornamenti UI, LiveData, View |
| Dispatchers.IO | Pool IO | 64 (limitedParallelism) | Rete, file, DB |
| Dispatchers.Default | Pool CPU | N core | Ordinamento, parsing, calcoli |
| Dispatchers.Unconfined | Thread corrente | N/D | Operazioni intermedie, test |
Dispatchers.Main è il dispatcher che esegue le coroutine sul thread principale di Android. È progettato per operazioni relative all'interfaccia: aggiornare TextView, chiamare notifyDataSetChanged, lavorare con LiveData e StateFlow. In Android, questo dispatcher è implementato tramite Handler (Looper.getMainLooper()).
// Correct switch to Main for UI updates
viewModelScope.launch(Dispatchers.IO) {
val data = repository.fetchData()
withContext(Dispatchers.Main) {
_uiState.value = data
}
}
Se una coroutine è già sul dispatcher Main, un withContext(Dispatchers.Main) aggiuntivo non crea overhead — il dispatcher controlla il thread corrente e salta la commutazione. withContext è il modo preferito per passare da un dispatcher all'altro.
Dispatchers.IO è un dispatcher ottimizzato per operazioni di I/O: richieste HTTP (Ktor, OkHttp), lettura e scrittura di file, lavoro con Room o SQLDelight. Utilizza un pool di 64 thread per impostazione predefinita, scalabile sotto carico. Ogni nuova richiesta I/O può creare un thread aggiuntivo fino al raggiungimento del limite.
Per controllare il numero di operazioni I/O simultanee, utilizzare limitedParallelism(). Questa funzione crea un nuovo dispatcher con un limite sul numero di thread paralleli, impedendo l'esaurimento del pool durante le operazioni di massa.
val limitedIo = Dispatchers.IO.limitedParallelism(4)
// Load 100 files with limit of 4 concurrent operations
coroutineScope {
val files = (1..100).map { index ->
async(limitedIo) {
downloadFile("file_$index")
}
}
files.awaitAll()
}
Utilizzare il dispatcher IO per tutte le operazioni in cui la coroutine trascorre il tempo in attesa (I/O-bound). I compiti intensivi di CPU sul dispatcher IO sono inefficienti — occupano thread destinati all'I/O, riducendo il throughput del sistema.
Dispatchers.Default è il dispatcher per le operazioni di calcolo che caricano il processore: ordinamento, filtraggio, parsing JSON (Moshi, Kotlinx Serialization), elaborazione immagini, calcoli. La dimensione del pool è pari al numero di core del processore (ma non meno di 2). Ciò garantisce il massimo utilizzo della CPU senza cambio di contesto.
suspend fun processData(input: List<RawRecord>): List<ProcessedRecord> {
return withContext(Dispatchers.Default) {
input
.parallelStream()
.map { transform(it) }
.toList()
}
}
Non utilizzare Dispatchers.Default per operazioni I/O — questo bloccherà i thread del pool CPU che potrebbero elaborare compiti computazionali. La separazione di IO e Default consente un utilizzo ottimale delle risorse di sistema: i thread IO attendono I/O, i thread CPU sono costantemente occupati con calcoli.
Dispatchers.Unconfined è un dispatcher speciale che non lega una coroutine a nessun pool. La coroutine inizia l'esecuzione nel thread in cui è stato chiamato launch/async, e dopo la sospensione riprende nel thread che ha chiamato resume. Questo comportamento è adatto per operazioni intermedie che non richiedono un contesto fisso.
fun main() = runBlocking {
launch(Dispatchers.Unconfined) {
println("Before delay: ${Thread.currentThread().getName()}")
delay(500L)
println("After delay: ${Thread.currentThread().getName()}")
}
}
Nel codice di produzione, Dispatchers.Unconfined è usato raramente. Casi d'uso principali: trasformazioni leggere prima di passare i dati a un altro dispatcher e test. Per carichi di produzione, utilizzare dispatcher espliciti — Unconfined è imprevedibile perché il thread di esecuzione dipende dall'implementazione di resume.
La selezione del dispatcher dipende dal tipo di attività: operazioni UI → Main, I/O-bound → IO, CPU-bound → Default, intermedie → ereditare dallo scope. Per Android, si consiglia di avviare una coroutine sul dispatcher in cui viene eseguito il lavoro principale e passare a Main tramite withContext prima di aggiornare l'UI.
Per scenari complessi, combinare i dispatcher con l'operatore +: Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler. Questo crea un CoroutineContext con un dispatcher specificato, gestione degli errori e una gerarchia Job isolata.
Domande Frequenti
Dispatchers.IO utilizza un pool fino a 64 thread per operazioni I/O-bound (attesa I/O), mentre Dispatchers.Default utilizza un pool basato sul numero di core CPU per compiti computazionali. Quando i thread scarseggiano, entrambi i pool possono condividere thread tra loro.
Sì, utilizzare newSingleThreadContext() per un singolo thread o newFixedThreadPoolContext() per un pool fisso. Per la produzione, utilizzare limitedParallelism() basato sui dispatcher esistenti — è più efficiente che creare nuovi pool.
Se Dispatchers.Main non è disponibile (ad esempio, in un test JUnit o servizio in background), viene lanciata una IllegalStateException. Utilizzare TestCoroutineDispatcher per i test e Dispatchers.IO o Default per i servizi in background.
Utilizzare Dispatchers.IO.limitedParallelism(N), dove N è il numero massimo di thread paralleli. Questo previene l'esaurimento del pool durante richieste di massa e fornisce parallelismo controllato.
Dispatchers.Unconfined è adatto per operazioni intermedie: trasformazioni leggere dei dati prima di passarli a un altro dispatcher, scenari di test. Nel codice Android di produzione, non è raccomandato a causa del thread di esecuzione indefinito dopo la sospensione.
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