Dispatchers w Kotlin Coroutines — komponenty CoroutineContext, określające wątki do wykonywania korutyn: Main (wątek UI), IO (sieć i dysk), Default (zadania CPU-intensywne) i Unconfined (bieżący wątek). Każdy dyspozytor zarządza wyspecjalizowaną pulą wątków, zoptymalizowaną pod konkretny typ pracy. Według przewodnika JetBrains, 2024, wybór odpowiedniego dyspozytora jest krytyczny dla wydajności i stabilności aplikacji.
Najważniejsze
Dispatchers — to implementacje interfejsu CoroutineDispatcher, które są elementami CoroutineContext. Określają one, na którym wątku lub puli wątków będzie wykonywana korutyna. Podczas tworzenia korutyny przez launch lub async dyspozytor może być przekazany jako pierwszy parametr: launch(Dispatchers.IO) { ... }. Jeśli dyspozytor nie jest określony, dziedziczy się z zewnętrznego CoroutineScope.
Kotlin dostarcza cztery wbudowane dyspozytory: Main, IO, Default, Unconfined. Każdy dyspozytor używa własnej puli wątków, zoptymalizowanej pod konkretny typ operacji. Prawidłowy wybór dyspozytora decyduje o wydajności aplikacji: błąd wyboru prowadzi do opóźnień interfejsu, marnowania rdzeni CPU lub nieefektywnego zużycia wątków.
| Dyspozytor | Pula wątków | Maks. wątków | Zastosowanie |
|---|---|---|---|
| Dispatchers.Main | Jeden (UI) | 1 | Aktualizacja UI, LiveData, View |
| Dispatchers.IO | Pula IO | 64 (limitedParallelism) | Sieć, pliki, bazy danych |
| Dispatchers.Default | Pula CPU | N rdzeni | Sortowanie, parsowanie, obliczenia |
| Dispatchers.Unconfined | Bieżący wątek | N/D | Operacje pośrednie, testy |
Dispatchers.Main — dyspozytor wykonujący korutyny na głównym wątku Androida. Jest przeznaczony do operacji związanych z interfejsem: aktualizacja TextView, wywołanie notifyDataSetChanged, praca z LiveData i StateFlow. W Androidzie ten dyspozytor jest zaimplementowany przez Handler (Looper.getMainLooper()).
// Prawidłowe przełączenie na Main dla aktualizacji UI
viewModelScope.launch(Dispatchers.IO) {
val data = repository.fetchData()
withContext(Dispatchers.Main) {
_uiState.value = data
}
}
Jeśli korutyna jest już na Main-dyspozytorze, dodatkowy withContext(Dispatchers.Main) nie tworzy narzutu — dyspozytor sprawdza bieżący wątek i pomija przełączenie. withContext jest preferowanym sposobem przełączania między dyspozytorami.
Dispatchers.IO — dyspozytor zoptymalizowany do operacji wejścia-wyjścia: zapytania HTTP (Ktor, OkHttp), odczyt i zapis plików, praca z Room lub SQLDelight. Używa puli 64 wątków domyślnie, skalowalnej pod obciążeniem. Każde nowe zapytanie IO może utworzyć dodatkowy wątek do osiągnięcia limitu.
Do kontroli liczby jednoczesnych operacji IO używaj limitedParallelism(). Ta funkcja tworzy nowy dyspozytor z ograniczeniem liczby równoległych wątków, zapobiegając wyczerpaniu puli przy masowych operacjach.
val limitedIo = Dispatchers.IO.limitedParallelism(4)
// Załaduj 100 plików z limitem 4 równoczesnych operacji
coroutineScope {
val files = (1..100).map { index ->
async(limitedIo) {
downloadFile("file_$index")
}
}
files.awaitAll()
}
Używaj dyspozytora IO dla wszystkich operacji, gdzie korutyna spędza czas na oczekiwaniu (I/O-bound). Zadania CPU-intensywne na dyspozytorze IO są nieefektywne — zajmują wątki przeznaczone do wejścia-wyjścia, obniżając przepustowość systemu.
Dispatchers.Default — dyspozytor do operacji obliczeniowych obciążających procesor: sortowanie, filtracja, parsowanie JSON (Moshi, Kotlinx Serialization), przetwarzanie obrazów, obliczenia. Rozmiar puli równa się liczbie rdzeni procesora (ale nie mniej niż 2). Zapewnia to maksymalne obciążenie CPU bez przełączania kontekstu.
suspend fun processData(input: List<RawRecord>): List<ProcessedRecord> {
return withContext(Dispatchers.Default) {
input
.parallelStream()
.map { transform(it) }
.toList()
}
}
Nie używaj Dispatchers.Default do operacji IO — to zablokuje wątki puli CPU, które mogłyby przetwarzać zadania obliczeniowe. Podział na IO i Default pozwala optymalnie wykorzystać zasoby systemowe: wątki IO czekają na wejście-wyjście, wątki CPU są stale obciążone obliczeniami.
Dispatchers.Unconfined — specjalny dyspozytor, który nie przypisuje korutyny do żadnej puli. Korutyna rozpoczyna wykonanie w tym wątku, w którym została wywołana launch/async, a po zawieszeniu wznawia się w wątku, który wywołał resume. To zachowanie jest odpowiednie dla operacji pośrednich, niewymagających stałego kontekstu.
fun main() = runBlocking {
launch(Dispatchers.Unconfined) {
println("Przed opóźnieniem: ${Thread.currentThread().getName()}")
delay(500L)
println("Po opóźnieniu: ${Thread.currentThread().getName()}")
}
}
W kodzie produkcyjnym Dispatchers.Unconfined jest stosowany rzadko. Główne przypadki: lekkie transformacje przed przekazaniem danych do innego dyspozytora oraz testy. Dla obciążenia produkcyjnego używaj jawnych dyspozytorów — Unconfined jest nieprzewidywalny, ponieważ wątek wykonania zależy od implementacji resume.
Wybór dyspozytora zależy od typu zadania: operacje UI → Main, I/O-bound → IO, CPU-bound → Default, pośrednie → dziedziczenie z scope. Dla Androida zaleca się uruchamianie korutyny na tym dyspozytorze, gdzie wykonywana jest główna praca, a przed aktualizacją UI przełączać się na Main przez withContext.
Dla złożonych scenariuszy łącz dyspozytory za pomocą operatora +: Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler. To tworzy CoroutineContext z określonym dyspozytorem, obsługą błędów i izolowaną hierarchią Job.
Często zadawane pytania
Dispatchers.IO używa puli do 64 wątków dla operacji I/O-bound (oczekiwanie na wejście-wyjście), a Dispatchers.Default — pulę według liczby rdzeni CPU dla zadań obliczeniowych. Przy braku wątków obie pule mogą dzielić się wątkami między sobą.
Tak, używaj newSingleThreadContext() dla jednowątkowego lub newFixedThreadPoolContext() dla stałej puli. Dla produkcji stosuj limitedParallelism() na bazie istniejących dyspozytorów — to efektywniejsze niż tworzenie nowych pul.
Jeśli Dispatchers.Main jest niedostępny (np. w teście JUnit lub usłudze tła), wyrzucany jest IllegalStateException. Do testów używaj TestCoroutineDispatcher, do usług tła — Dispatchers.IO lub Default.
Używaj Dispatchers.IO.limitedParallelism(N), gdzie N — maksymalna liczba równoległych wątków. To zapobiega wyczerpaniu puli przy masowych zapytaniach i daje kontrolowaną równoległość.
Dispatchers.Unconfined jest odpowiedni dla operacji pośrednich: lekkie transformacje danych przed przekazaniem do innego dyspozytora, scenariusze testowe. W kodzie produkcyjnym Androida nie jest zalecany z powodu nieokreślonego wątku wykonania po zawieszeniu.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również