Dispatchers în Kotlin Coroutines — componente ale CoroutineContext care determină firele de execuție pentru corutine: Main (fir UI), IO (rețea și disc), Default (sarcini CPU-intensive) și Unconfined (firul curent). Fiecare dispatcher gestionează un pool specializat de fire, optimizat pentru un tip specific de muncă. Conform ghidului JetBrains, 2024, alegerea dispatcherului potrivit este critică pentru performanța și stabilitatea aplicației.
Principalele puncte
Dispatchers — sunt implementări ale interfeței CoroutineDispatcher, care sunt elemente ale CoroutineContext. Ele determină pe ce fir sau pool de fire va fi executată corutina. La crearea unei corutine prin launch sau async, dispatcherul poate fi transmis ca prim parametru: launch(Dispatchers.IO) { ... }. Dacă dispatcherul nu este specificat, se moștenește din CoroutineScope extern.
Kotlin oferă patru dispatchere încorporate: Main, IO, Default, Unconfined. Fiecare dispatcher folosește propriul pool de fire, optimizat pentru un tip specific de operații. Alegerea corectă a dispatcherului determină performanța aplicației: o alegere greșită duce la întârzieri ale interfeței, la subutilizarea nucleelor CPU sau la consumul ineficient al firelor.
| Dispatcher | Pool de fire | Max. fire | Utilizare |
|---|---|---|---|
| Dispatchers.Main | Unul (UI) | 1 | Actualizare UI, LiveData, View |
| Dispatchers.IO | Pool IO | 64 (limitedParallelism) | Rețea, fișiere, baze de date |
| Dispatchers.Default | Pool CPU | N nuclee | Sortare, parsare, calcule |
| Dispatchers.Unconfined | Firul curent | N/A | Operații intermediare, teste |
Dispatchers.Main — dispatcherul care execută corutinele pe firul principal Android. Este destinat operațiilor legate de interfață: actualizare TextView, apelare notifyDataSetChanged, lucrul cu LiveData și StateFlow. În Android, acest dispatcher este implementat prin Handler (Looper.getMainLooper()).
// Comutare corectă la Main pentru actualizări UI
viewModelScope.launch(Dispatchers.IO) {
val data = repository.fetchData()
withContext(Dispatchers.Main) {
_uiState.value = data
}
}
Dacă corutina este deja pe dispatcherul Main, un withContext(Dispatchers.Main) suplimentar nu creează costuri suplimentare — dispatcherul verifică firul curent și omite comutarea. withContext este modul preferat de a comuta între dispatchere.
Dispatchers.IO — dispatcher optimizat pentru operații de intrare-ieșire: cereri HTTP (Ktor, OkHttp), citirea și scrierea fișierelor, lucrul cu Room sau SQLDelight. Folosește un pool de 64 de fire în mod implicit, scalabil sub sarcină. Fiecare nouă cerere IO poate crea un fir suplimentar până la atingerea limitei.
Pentru a controla numărul de operații IO simultane, utilizați limitedParallelism(). Această funcție creează un nou dispatcher cu limitarea numărului de fire paralele, prevenind epuizarea pool-ului în operații masive.
val limitedIo = Dispatchers.IO.limitedParallelism(4)
// Încarcă 100 de fișiere cu limita de 4 operații concurente
coroutineScope {
val files = (1..100).map { index ->
async(limitedIo) {
downloadFile("file_$index")
}
}
files.awaitAll()
}
Utilizați dispatcherul IO pentru toate operațiile în care corutina petrece timp în așteptare (I/O-bound). Sarcinile CPU-intensive pe dispatcherul IO sunt ineficiente — ele ocupă fire destinate intrării-ieșirii, reducând capacitatea de transfer a sistemului.
Dispatchers.Default — dispatcher pentru operații de calcul care încarcă procesorul: sortare, filtrare, parsare JSON (Moshi, Kotlinx Serialization), procesare imagini, calcule. Dimensiunea pool-ului este egală cu numărul de nuclee ale procesorului (dar nu mai puțin de 2). Aceasta asigură încărcarea maximă a CPU fără comutare de context.
suspend fun processData(input: List<RawRecord>): List<ProcessedRecord> {
return withContext(Dispatchers.Default) {
input
.parallelStream()
.map { transform(it) }
.toList()
}
}
Nu utilizați Dispatchers.Default pentru operații IO — aceasta va bloca firele pool-ului CPU care ar putea procesa sarcini de calcul. Separarea în IO și Default permite utilizarea optimă a resurselor sistemului: firele IO așteaptă intrare-ieșire, firele CPU sunt ocupate constant cu calcule.
Dispatchers.Unconfined — un dispatcher special care nu leagă corutina de niciun pool. Corutina începe execuția în același fir în care a fost apelată launch/async, iar după suspendare se reia în firul care a apelat resume. Acest comportament este potrivit pentru operații intermediare care nu necesită un context fix.
fun main() = runBlocking {
launch(Dispatchers.Unconfined) {
println("Înainte de întârziere: ${Thread.currentThread().getName()}")
delay(500L)
println("După întârziere: ${Thread.currentThread().getName()}")
}
}
În codul de producție, Dispatchers.Unconfined este folosit rar. Cazurile principale: transformări ușoare înainte de transmiterea datelor către un alt dispatcher și teste. Pentru sarcinile de producție, utilizați dispatchere explicite — Unconfined este imprevizibil, deoarece firul de execuție depinde de implementarea resume.
Alegerea dispatcherului este determinată de tipul sarcinii: operații UI → Main, I/O-bound → IO, CPU-bound → Default, intermediare → moștenire din scope. Pentru Android, se recomandă să porniți corutina pe dispatcherul unde se execută munca principală, iar înainte de actualizarea UI să comutați la Main prin withContext.
Pentru scenarii complexe, combinați dispatcherele cu operatorul +: Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler. Aceasta creează un CoroutineContext cu dispatcherul specificat, gestionarea erorilor și o ierarhie Job izolată.
Întrebări frecvente
Dispatchers.IO folosește un pool de până la 64 de fire pentru operații I/O-bound (așteptare intrare-ieșire), iar Dispatchers.Default — un pool în funcție de numărul de nuclee CPU pentru sarcini de calcul. La insuficiența firelor, ambele pool-uri pot împărți fire între ele.
Da, utilizați newSingleThreadContext() pentru unul mono-fir sau newFixedThreadPoolContext() pentru un pool fix. Pentru producție, aplicați limitedParallelism() pe baza dispatcherelor existente — este mai eficient decât crearea de noi pool-uri.
Dacă Dispatchers.Main nu este disponibil (de exemplu, într-un test JUnit sau serviciu de fundal), se aruncă IllegalStateException. Pentru teste, utilizați TestCoroutineDispatcher, pentru servicii de fundal — Dispatchers.IO sau Default.
Utilizați Dispatchers.IO.limitedParallelism(N), unde N este numărul maxim de fire paralele. Aceasta previne epuizarea pool-ului la cereri masive și oferă un paralelism controlat.
Dispatchers.Unconfined este potrivit pentru operații intermediare: transformări ușoare de date înainte de transmiterea către un alt dispatcher, scenarii de test. În codul de producție Android nu este recomandat din cauza firului de execuție incert după suspendare.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și