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 | Поточний потік | N/A | Проміжні операції, тести |
Dispatchers.Main — диспетчер, що виконує корутини на головному потоці Android. Він призначений для операцій, пов'язаних з інтерфейсом: оновлення TextView, виклик notifyDataSetChanged, робота з LiveData та StateFlow. В Android цей диспетчер реалізований через Handler (Looper.getMainLooper()).
// Correct switch to Main for UI updates
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)
// Load 100 files with limit of 4 concurrent operations
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("Before delay: ${Thread.currentThread().getName()}")
delay(500L)
println("After delay: ${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() для фіксованого пулу. Для продакшену застосовуйте limitedParallelism() на основі існуючих диспетчерів — це ефективніше, ніж створення нових пулів.
Якщо Dispatchers.Main недоступний (наприклад, у JUnit-тесті або фоновому сервісі), викидається IllegalStateException. Для тестів використовуйте TestCoroutineDispatcher, для фонових сервісів — Dispatchers.IO або Default.
Використовуйте Dispatchers.IO.limitedParallelism(N), де N — максимальна кількість паралельних потоків. Це запобігає виснаженню пулу при масових запитах і дає контрольований паралелізм.
Dispatchers.Unconfined підходить для проміжних операцій: легкі трансформації даних перед передачею в інший диспетчер, тестові сценарії. У production-коді Android не рекомендується через невизначений потік виконання після призупинення.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також