Dispatchers в Kotlin Coroutines — компоненти на CoroutineContext, които определят нишките за изпълнение на корутините: Main (UI нишка), IO (мрежа и диск), Default (CPU-интензивни задачи) и Unconfined (текуща нишка). Всеки диспечер управлява специализиран пул от нишки, оптимизиран за конкретен тип работа. Според ръководството на JetBrains, 2024, изборът на правилния диспечер е критичен за производителността и стабилността на приложението.
Основни точки
Dispatchers — имплементации на интерфейса CoroutineDispatcher, които са елементи на CoroutineContext. Те определят на коя нишка или пул от нишки ще се изпълнява корутината. При създаване на корутина чрез launch или async, диспечерът може да бъде предаден като първи параметър: launch(Dispatchers.IO) { ... }. Ако диспечерът не е указан, се наследява от външния CoroutineScope.
Kotlin предоставя четири вградени диспечера: Main, IO, Default, Unconfined. Всеки диспечер използва собствен пул от нишки, оптимизиран за конкретен тип операции. Правилният избор на диспечер определя производителността на приложението: грешен избор води до забавяне на интерфейса, неизползване на CPU ядра или неефективно изразходване на нишки.
| Диспечер | Пул нишки | Макс. нишки | Приложение |
|---|---|---|---|
| Dispatchers.Main | Една (UI) | 1 | Актуализиране на UI, LiveData, View |
| Dispatchers.IO | IO пул | 64 (limitedParallelism) | Мрежа, файлове, бази данни |
| Dispatchers.Default | CPU пул | N ядра | Сортиране, парсване, изчисления |
| Dispatchers.Unconfined | Текуща нишка | Н/П | Междинни операции, тестове |
Dispatchers.Main — диспечер, изпълняващ корутини на главната нишка на Android. Предназначен е за операции, свързани с интерфейса: актуализиране на TextView, извикване на notifyDataSetChanged, работа с LiveData и StateFlow. В Android този диспечер е имплементиран чрез Handler (Looper.getMainLooper()).
// Правилно превключване към Main за UI актуализации
viewModelScope.launch(Dispatchers.IO) {
val data = repository.fetchData()
withContext(Dispatchers.Main) {
_uiState.value = data
}
}
Ако корутината вече е на Main-диспечера, допълнителният withContext(Dispatchers.Main) не създава допълнително натоварване — диспечерът проверява текущата нишка и пропуска превключването. withContext е предпочитаният начин за превключване между диспечери.
Dispatchers.IO — диспечер, оптимизиран за входно-изходни операции: HTTP заявки (Ktor, OkHttp), четене и запис на файлове, работа с Room или SQLDelight. По подразбиране използва пул от 64 нишки, мащабируем под натоварване. Всяка нова IO заявка може да създаде допълнителна нишка до достигане на лимита.
За контрол на броя едновременни IO операции използвайте limitedParallelism(). Тази функция създава нов диспечер с ограничение на броя паралелни нишки, предотвратявайки изчерпване на пула при масови операции.
val limitedIo = Dispatchers.IO.limitedParallelism(4)
// Зареди 100 файла с лимит от 4 едновременни операции
coroutineScope {
val files = (1..100).map { index ->
async(limitedIo) {
downloadFile("file_$index")
}
}
files.awaitAll()
}
Използвайте IO-диспечера за всички операции, където корутината прекарва време в чакане (I/O-bound). CPU-интензивните задачи на IO-диспечера са неефективни — те заемат нишки, предназначени за вход-изход, намалявайки пропускателната способност на системата.
Dispatchers.Default — диспечер за изчислителни операции, натоварващи процесора: сортиране, филтриране, парсване на JSON (Moshi, Kotlinx Serialization), обработка на изображения, изчисления. Размерът на пула е равен на броя на ядрата на процесора (но не по-малко от 2). Това осигурява максимално натоварване на CPU без превключване на контекст.
suspend fun processData(input: List<RawRecord>): List<ProcessedRecord> {
return withContext(Dispatchers.Default) {
input
.parallelStream()
.map { transform(it) }
.toList()
}
}
Не използвайте Dispatchers.Default за IO операции — това ще блокира нишките на CPU пула, които биха могли да обработват изчислителни задачи. Разделянето на IO и Default позволява оптимално използване на системните ресурси: IO нишките чакат вход-изход, CPU нишките са постоянно заети с изчисления.
Dispatchers.Unconfined — специален диспечер, който не обвързва корутината с никакъв пул. Корутината започва изпълнение в същата нишка, в която е извикана launch/async, а след спиране продължава в нишката, която е извикала resume. Това поведение е подходящо за междинни операции, които не изискват фиксиран контекст.
fun main() = runBlocking {
launch(Dispatchers.Unconfined) {
println("Преди закъснение: ${Thread.currentThread().getName()}")
delay(500L)
println("След закъснение: ${Thread.currentThread().getName()}")
}
}
В production код Dispatchers.Unconfined се използва рядко. Основни случаи: леки трансформации преди предаване на данни на друг диспечер и тестове. За production натоварване използвайте изрични диспечери — Unconfined е непредсказуем, тъй като нишката на изпълнение зависи от имплементацията на resume.
Изборът на диспечер се определя от типа задача: UI операции → Main, I/O-bound → IO, CPU-bound → Default, междинни → наследяване от scope. За Android се препоръчва корутината да се стартира на диспечера, където се извършва основната работа, а преди актуализиране на UI да се превключи на Main чрез withContext.
За сложни сценарии комбинирайте диспечери с оператора +: Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler. Това създава CoroutineContext с определен диспечер, обработка на грешки и изолирана Job йерархия.
Често задавани въпроси
Dispatchers.IO използва пул до 64 нишки за I/O-bound операции (чакане на вход-изход), а Dispatchers.Default — пул според броя на CPU ядрата за изчислителни задачи. При недостиг на нишки двата пула могат да споделят нишки помежду си.
Да, използвайте newSingleThreadContext() за еднонишков или newFixedThreadPoolContext() за фиксиран пул. За production прилагайте limitedParallelism() на базата на съществуващи диспечери — това е по-ефективно от създаването на нови пулове.
Ако Dispatchers.Main не е наличен (напр. в JUnit тест или фонова услуга), се хвърля IllegalStateException. За тестове използвайте TestCoroutineDispatcher, за фонови услуги — Dispatchers.IO или Default.
Използвайте Dispatchers.IO.limitedParallelism(N), където N е максималният брой паралелни нишки. Това предотвратява изчерпване на пула при масови заявки и осигурява контролиран паралелизъм.
Dispatchers.Unconfined е подходящ за междинни операции: леки трансформации на данни преди предаване на друг диспечер, тестови сценарии. В Android production код не се препоръчва поради неопределена нишка на изпълнение след спиране.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също