Dispatchers in Kotlin Coroutines — componenten van CoroutineContext die de threads bepalen voor de uitvoering van coroutines: Main (UI-thread), IO (netwerk en schijf), Default (CPU-intensieve taken) en Unconfined (huidige thread). Elke dispatcher beheert een gespecialiseerde threadpool, geoptimaliseerd voor een specifiek type werk. Volgens de handleiding van JetBrains, 2024, is de keuze van de juiste dispatcher cruciaal voor de prestaties en stabiliteit van de applicatie.
Belangrijkste punten
Dispatchers — zijn implementaties van de CoroutineDispatcher-interface, die elementen zijn van CoroutineContext. Ze bepalen op welke thread of threadpool de coroutine wordt uitgevoerd. Bij het maken van een coroutine via launch of async kan de dispatcher als eerste parameter worden doorgegeven: launch(Dispatchers.IO) { ... }. Als er geen dispatcher is opgegeven, wordt deze overgenomen van de externe CoroutineScope.
Kotlin biedt vier ingebouwde dispatchers: Main, IO, Default, Unconfined. Elke dispatcher gebruikt zijn eigen threadpool, geoptimaliseerd voor een specifiek type operatie. De juiste keuze van de dispatcher bepaalt de prestaties van de applicatie: een verkeerde keuze leidt tot vertragingen in de interface, het onbenut laten van CPU-kernen of inefficiënt gebruik van threads.
| Dispatcher | Threadpool | Max. threads | Toepassing |
|---|---|---|---|
| Dispatchers.Main | Eén (UI) | 1 | UI bijwerken, LiveData, View |
| Dispatchers.IO | IO-pool | 64 (limitedParallelism) | Netwerk, bestanden, databases |
| Dispatchers.Default | CPU-pool | N kernen | Sorteren, parsen, berekeningen |
| Dispatchers.Unconfined | Huidige thread | NVT | Tussenbewerkingen, tests |
Dispatchers.Main — dispatcher die coroutines uitvoert op de hoofdthread van Android. Het is bedoeld voor bewerkingen die verband houden met de interface: TextView bijwerken, notifyDataSetChanged aanroepen, werken met LiveData en StateFlow. In Android is deze dispatcher geïmplementeerd via Handler (Looper.getMainLooper()).
// Correct overschakelen naar Main voor UI-updates
viewModelScope.launch(Dispatchers.IO) {
val data = repository.fetchData()
withContext(Dispatchers.Main) {
_uiState.value = data
}
}
Als de coroutine zich al op de Main-dispatcher bevindt, creëert extra withContext(Dispatchers.Main) geen overhead — de dispatcher controleert de huidige thread en slaat de omschakeling over. withContext is de geprefereerde manier om tussen dispatchers te schakelen.
Dispatchers.IO — dispatcher geoptimaliseerd voor invoer-uitvoerbewerkingen: HTTP-verzoeken (Ktor, OkHttp), lezen en schrijven van bestanden, werken met Room of SQLDelight. Het gebruikt standaard een pool van 64 threads, schaalbaar onder belasting. Elk nieuw IO-verzoek kan een extra thread creëren tot de limiet is bereikt.
Gebruik limitedParallelism() om het aantal gelijktijdige IO-bewerkingen te beheren. Deze functie creëert een nieuwe dispatcher met een beperking van het aantal parallelle threads, waardoor uitputting van de pool bij massale bewerkingen wordt voorkomen.
val limitedIo = Dispatchers.IO.limitedParallelism(4)
// Laad 100 bestanden met een limiet van 4 gelijktijdige bewerkingen
coroutineScope {
val files = (1..100).map { index ->
async(limitedIo) {
downloadFile("file_$index")
}
}
files.awaitAll()
}
Gebruik de IO-dispatcher voor alle bewerkingen waarbij de coroutine tijd besteedt aan wachten (I/O-bound). CPU-intensieve taken op de IO-dispatcher zijn inefficiënt — ze bezetten threads die bedoeld zijn voor invoer-uitvoer, waardoor de doorvoer van het systeem afneemt.
Dispatchers.Default — dispatcher voor rekenintensieve bewerkingen die de processor belasten: sorteren, filteren, JSON-parsen (Moshi, Kotlinx Serialization), beeldverwerking, berekeningen. De poolgrootte is gelijk aan het aantal processorkernen (maar niet minder dan 2). Dit zorgt voor maximale CPU-belasting zonder contextomschakeling.
suspend fun processData(input: List<RawRecord>): List<ProcessedRecord> {
return withContext(Dispatchers.Default) {
input
.parallelStream()
.map { transform(it) }
.toList()
}
}
Gebruik Dispatchers.Default niet voor IO-bewerkingen — dit blokkeert threads van de CPU-pool die rekentaken zouden kunnen verwerken. De scheiding in IO en Default maakt optimaal gebruik van systeembronnen mogelijk: IO-threads wachten op invoer-uitvoer, CPU-threads zijn constant bezig met berekeningen.
Dispatchers.Unconfined — een speciale dispatcher die de coroutine niet aan een pool bindt. De coroutine begint met uitvoeren in de thread waarin launch/async werd aangeroepen en wordt na onderbreking hervat in de thread die resume aanriep. Dit gedrag is geschikt voor tussenliggende bewerkingen die geen vaste context vereisen.
fun main() = runBlocking {
launch(Dispatchers.Unconfined) {
println("Voor vertraging: ${Thread.currentThread().getName()}")
delay(500L)
println("Na vertraging: ${Thread.currentThread().getName()}")
}
}
In productiecode wordt Dispatchers.Unconfined zelden gebruikt. Belangrijkste gevallen: lichte transformaties voordat gegevens naar een andere dispatcher worden gestuurd en tests. Gebruik voor productiebelasting expliciete dispatchers — Unconfined is onvoorspelbaar omdat de uitvoeringsthread afhankelijk is van de implementatie van resume.
De keuze van de dispatcher wordt bepaald door het taaktype: UI-bewerkingen → Main, I/O-bound → IO, CPU-bound → Default, tussenliggend → overerving van scope. Voor Android wordt aanbevolen de coroutine te starten op de dispatcher waar het hoofdwerk wordt uitgevoerd en voor UI-updates over te schakelen naar Main via withContext.
Combineer voor complexe scenario's dispatchers met de operator +: Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler. Dit creëert een CoroutineContext met een gespecificeerde dispatcher, foutafhandeling en een geïsoleerde Job-hiërarchie.
Veelgestelde vragen
Dispatchers.IO gebruikt een pool van maximaal 64 threads voor I/O-bound bewerkingen (wachten op invoer-uitvoer), en Dispatchers.Default een pool op basis van het aantal CPU-kernen voor rekentaken. Bij tekort aan threads kunnen beide pools threads met elkaar delen.
Ja, gebruik newSingleThreadContext() voor een single-thread of newFixedThreadPoolContext() voor een vaste pool. Gebruik voor productie limitedParallelism() op basis van bestaande dispatchers — dit is efficiënter dan het creëren van nieuwe pools.
Als Dispatchers.Main niet beschikbaar is (bijv. in een JUnit-test of achtergrondservice), wordt IllegalStateException gegooid. Gebruik voor tests TestCoroutineDispatcher, voor achtergrondservices Dispatchers.IO of Default.
Gebruik Dispatchers.IO.limitedParallelism(N), waarbij N het maximale aantal parallelle threads is. Dit voorkomt uitputting van de pool bij massale verzoeken en biedt gecontroleerd parallellisme.
Dispatchers.Unconfined is geschikt voor tussenbewerkingen: lichte datatransformaties voordat gegevens naar een andere dispatcher worden gestuurd en testscenario's. In Android-productiecode wordt het afgeraden vanwege de onbepaalde uitvoeringsthread na onderbreking.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook