Dispatchers in Kotlin Coroutines sind CoroutineContext-Komponenten, die die Threads für die Ausführung von Coroutinen bestimmen: Main (UI-Thread), IO (Netzwerk und Festplatte), Default (CPU-intensive Aufgaben) und Unconfined (aktueller Thread). Jeder Dispatcher verwaltet einen spezialisierten Thread-Pool, der für eine bestimmte Art von Arbeit optimiert ist. Laut JetBrains-Leitfaden, 2024 ist die Wahl des richtigen Dispatchers entscheidend für die Leistung und Stabilität der Anwendung.
Wichtige Erkenntnisse
Dispatchers sind Implementierungen des CoroutineDispatcher-Interfaces, die Elemente von CoroutineContext sind. Sie bestimmen, in welchem Thread oder Thread-Pool die Coroutine ausgeführt wird. Beim Erstellen einer Coroutine über launch oder async kann der Dispatcher als erster Parameter übergeben werden: launch(Dispatchers.IO) { ... }. Wenn kein Dispatcher angegeben ist, wird er vom äußeren CoroutineScope geerbt.
Kotlin bietet vier integrierte Dispatcher: Main, IO, Default, Unconfined. Jeder Dispatcher verwendet seinen eigenen Thread-Pool, der für eine bestimmte Art von Operation optimiert ist. Die Wahl des richtigen Dispatchers bestimmt die Anwendungsleistung: Eine falsche Wahl führt zu UI-Verzögerungen, untätigen CPU-Kernen oder ineffizienter Thread-Nutzung.
| Dispatcher | Thread-Pool | Max. Threads | Verwendung |
|---|---|---|---|
| Dispatchers.Main | Einer (UI) | 1 | UI-Updates, LiveData, View |
| Dispatchers.IO | IO-Pool | 64 (limitedParallelism) | Netzwerk, Dateien, DB |
| Dispatchers.Default | CPU-Pool | N Kerne | Sortieren, Parsen, Berechnungen |
| Dispatchers.Unconfined | Aktueller Thread | N/V | Zwischenoperationen, Tests |
Dispatchers.Main ist der Dispatcher, der Coroutinen auf dem Android-Hauptthread ausführt. Er ist für UI-bezogene Operationen konzipiert: Aktualisieren von TextView, Aufrufen von notifyDataSetChanged, Arbeiten mit LiveData und StateFlow. In Android wird dieser Dispatcher über Handler (Looper.getMainLooper()) implementiert.
// Correct switch to Main for UI updates
viewModelScope.launch(Dispatchers.IO) {
val data = repository.fetchData()
withContext(Dispatchers.Main) {
_uiState.value = data
}
}
Wenn sich eine Coroutine bereits im Main-Dispatcher befindet, erzeugt ein zusätzliches withContext(Dispatchers.Main) keinen Overhead — der Dispatcher überprüft den aktuellen Thread und überspringt die Umschaltung. withContext ist die bevorzugte Methode zum Wechseln zwischen Dispatchern.
Dispatchers.IO ist ein für E/A-Operationen optimierter Dispatcher: HTTP-Anfragen (Ktor, OkHttp), Lesen und Schreiben von Dateien, Arbeiten mit Room oder SQLDelight. Er verwendet standardmäßig einen Pool von 64 Threads, der unter Last skalierbar ist. Jede neue E/A-Anfrage kann einen zusätzlichen Thread erstellen, bis das Limit erreicht ist.
Um die Anzahl gleichzeitiger E/A-Operationen zu steuern, verwenden Sie limitedParallelism(). Diese Funktion erstellt einen neuen Dispatcher mit einer Begrenzung der Anzahl paralleler Threads und verhindert so die Erschöpfung des Pools bei Massenoperationen.
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()
}
Verwenden Sie den IO-Dispatcher für alle Operationen, bei denen die Coroutine Zeit mit Warten verbringt (I/O-gebunden). CPU-intensive Aufgaben auf dem IO-Dispatcher sind ineffizient — sie belegen Threads, die für E/A bestimmt sind, und verringern den Durchsatz des Systems.
Dispatchers.Default ist der Dispatcher für Rechenoperationen, die den Prozessor belasten: Sortieren, Filtern, JSON-Parsing (Moshi, Kotlinx Serialization), Bildverarbeitung, Berechnungen. Die Poolgröße entspricht der Anzahl der Prozessorkerne (jedoch nicht weniger als 2). Dies gewährleistet eine maximale CPU-Auslastung ohne Kontextwechsel.
suspend fun processData(input: List<RawRecord>): List<ProcessedRecord> {
return withContext(Dispatchers.Default) {
input
.parallelStream()
.map { transform(it) }
.toList()
}
}
Verwenden Sie Dispatchers.Default nicht für E/A-Operationen — dies blockiert Threads des CPU-Pools, die Rechenaufgaben verarbeiten könnten. Die Trennung von IO und Default ermöglicht eine optimale Nutzung der Systemressourcen: IO-Threads warten auf E/A, CPU-Threads sind ständig mit Berechnungen beschäftigt.
Dispatchers.Unconfined ist ein spezieller Dispatcher, der eine Coroutine an keinen Pool bindet. Die Coroutine beginnt die Ausführung in dem Thread, in dem launch/async aufgerufen wurde, und wird nach der Aussetzung in dem Thread fortgesetzt, der resume aufgerufen hat. Dieses Verhalten eignet sich für Zwischenoperationen, die keinen festen Kontext erfordern.
fun main() = runBlocking {
launch(Dispatchers.Unconfined) {
println("Before delay: ${Thread.currentThread().getName()}")
delay(500L)
println("After delay: ${Thread.currentThread().getName()}")
}
}
In Produktionscode wird Dispatchers.Unconfined selten verwendet. Hauptanwendungsfälle: leichte Transformationen vor der Übergabe von Daten an einen anderen Dispatcher und Tests. Für Produktionslasten verwenden Sie explizite Dispatcher — Unconfined ist unvorhersehbar, da der Ausführungsthread von der Resume-Implementierung abhängt.
Die Dispatcher-Auswahl hängt vom Aufgabentyp ab: UI-Operationen → Main, I/O-gebunden → IO, CPU-gebunden → Default, Zwischenoperationen → vom Gültigkeitsbereich erben. Für Android wird empfohlen, eine Coroutine auf dem Dispatcher zu starten, auf dem die Hauptarbeit ausgeführt wird, und vor dem Aktualisieren der UI über withContext auf Main umzuschalten.
Für komplexe Szenarien kombinieren Sie Dispatcher mit dem +-Operator: Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler. Dies erstellt einen CoroutineContext mit einem bestimmten Dispatcher, Fehlerbehandlung und einer isolierten Job-Hierarchie.
Häufig gestellte Fragen
Dispatchers.IO verwendet einen Pool von bis zu 64 Threads für I/O-gebundene Operationen (Warten auf E/A), während Dispatchers.Default einen Pool basierend auf der Anzahl der CPU-Kerne für Rechenaufgaben verwendet. Bei Thread-Knappheit können beide Pools Threads miteinander teilen.
Ja, verwenden Sie newSingleThreadContext() für einen Einzelthread oder newFixedThreadPoolContext() für einen festen Pool. Für die Produktion verwenden Sie limitedParallelism() basierend auf vorhandenen Dispatchern — dies ist effizienter als das Erstellen neuer Pools.
Wenn Dispatchers.Main nicht verfügbar ist (z.B. in einem JUnit-Test oder Hintergrunddienst), wird eine IllegalStateException ausgelöst. Verwenden Sie TestCoroutineDispatcher für Tests und Dispatchers.IO oder Default für Hintergrunddienste.
Verwenden Sie Dispatchers.IO.limitedParallelism(N), wobei N die maximale Anzahl paralleler Threads ist. Dies verhindert die Erschöpfung des Pools bei Massenanfragen und bietet kontrollierte Parallelität.
Dispatchers.Unconfined eignet sich für Zwischenoperationen: leichte Datentransformationen vor der Übergabe an einen anderen Dispatcher, Testszenarien. In produktivem Android-Code wird es aufgrund des undefinierten Ausführungsthreads nach der Aussetzung nicht empfohlen.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch