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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также