1. Введение
В этом руководстве мы познакомимся с CoroutineContext, а затем подробно рассмотрим диспетчеры — один из ключевых элементов CoroutineContext.
2. Что такое корутина?
Корутины — это подпрограммы или процессы, поддерживающие кооперативную многозадачность. Это значит, что корутина может быть приостановлена, возобновлена или уступить управление другой корутине.
В Kotlin ключевое слово suspend перед функцией означает, что функция приостанавливает вызывающего и может быть вызвана только внутри корутины.
Существует несколько функций-строителей корутин:
- launch
- async
- runBlocking (блокирующая корутина, часто используется для запуска в main).
3. Контекст корутины (CoroutineContext)
Каждая корутина имеет связанный с ней контекст — CoroutineContext, который представляет собой индексированное множество элементов (Elements).
Что такое индексированное множество? Это гибрид множества и словаря: у каждого элемента есть уникальный ключ.
Метод CoroutineContext.get позволяет безопасно по типу получить нужный элемент:
public operator fun <E : Element> get(key: Key<E>): E?
Все корутинные классы реализуют интерфейс CoroutineScope и имеют свойство coroutineContext. Поэтому внутри любой корутины можно получить доступ к текущему контексту:
runBlocking {
Assertions.assertNotNull(coroutineContext)
}
Также можно прочитать конкретный элемент контекста:
runBlocking {
Assertions.assertNotNull(coroutineContext[Job])
}
3.1. Как изменять контекст корутины?
CoroutineContext является неизменяемым, но можно создать новый контекст, добавив или удалив элемент, либо объединив два контекста.
- Пустой контекст представлен как EmptyCoroutineContext.
- Объединение контекстов осуществляется через оператор +.
Пример добавления имени:
kotlinКопироватьРедактироватьval context = EmptyCoroutineContext
val newContext = context + CoroutineName("baeldung")
Assertions.assertTrue(newContext != context)
Assertions.assertEquals("baeldung", newContext[CoroutineName]!!.name)
Пример удаления элемента:
kotlinКопироватьРедактироватьval context = CoroutineName("baeldung")
val newContext = context.minusKey(CoroutineName)
Assertions.assertNull(newContext[CoroutineName])
3.2. Элементы контекста корутины
Kotlin предоставляет несколько встроенных реализаций CoroutineContext.Element, отвечающих за разные аспекты поведения корутин:
- Отладка: CoroutineName, CoroutineId
Используются для назначения имени и идентификатора корутине, что упрощает логирование и трассировку выполнения. - Управление жизненным циклом: Job
Сохраняет иерархию задач и позволяет управлять жизненным циклом корутины (например, отменять или отслеживать завершение). - Обработка исключений: CoroutineExceptionHandler
Обрабатывает исключения, возникшие в корутинах, созданных через такие билдеры, как launch, которые не возвращают результат и не передают исключения дальше по цепочке. - Управление потоками: ContinuationInterceptor
Прослушивает и перехватывает возобновление выполнения корутины. Наиболее распространённые реализации — это диспетчеры (CoroutineDispatcher).
По умолчанию элементом ContinuationInterceptor является Dispatchers.Default.
Наиболее часто используемые элементы — это диспетчеры (CoroutineDispatcher), которые реализуют ContinuationInterceptor и определяют, в каком потоке или пуле потоков будет выполняться корутина.
4. Диспетчеры (Dispatchers)
CoroutineDispatcher — это подтип элемента контекста ContinuationInterceptor. Следовательно, он отвечает за определение потока (или пула потоков), в котором будет выполняться корутина.
При запуске корутины Kotlin сначала вызывает метод CoroutineDispatcher.isDispatchNeeded. Если он возвращает true, тогда выполняется dispatch, и назначается поток исполнения. Если false — корутина исполняется в непривязанном режиме (unconfined) в вызывающем потоке.
Kotlin предоставляет несколько реализаций CoroutineDispatcher, в том числе внутренние синглтоны:
- DefaultScheduler
- CommonPool
- DefaultExecutor
- Unconfined
Для использования предопределённых диспетчеров применяются значения из kotlinx.coroutines.Dispatchers:
- Dispatchers.Default
Если не задана системная переменная kotlinx.coroutines.scheduler, используется DefaultScheduler. Иначе — CommonPool. - Dispatchers.Main
Загрузчик главного диспетчера. Доступен только при наличии нужной зависимости в classpath. - Dispatchers.Unconfined
Ссылается на Unconfined singleton. - Dispatchers.IO
Используется для IO-операций. Ссылается на DefaultScheduler.IO.
Пример передачи диспетчера в корутину:
launch(Dispatchers.Default) {
Assertions.assertTrue(
coroutineContext[ContinuationInterceptor]!!
.javaClass
.name.contains("DefaultScheduler")
)
}
Также в ThreadPoolDispatcher.kt есть устаревшие функции:
- newSingleThreadContext — для выполнения в одном потоке
- newFixedThreadPoolContext — для фиксированного пула потоков
Рекомендуется использовать ExecutorService и передавать его через asCoroutineDispatcher():
launch(Executors.newSingleThreadExecutor().asCoroutineDispatcher()) {
Assertions.assertTrue(
coroutineContext[ContinuationInterceptor]!!
.javaClass
.name.contains("ExecutorCoroutineDispatcher")
)
}
4.1. Confined (Привязанные) vs. Unconfined (Непривязанные) диспетчеры
По умолчанию корутина наследует диспетчер от внешнего CoroutineScope, если явно не указан другой:
runBlocking(Executors.newSingleThreadExecutor().asCoroutineDispatcher()) {
launch {
Assertions.assertTrue(
coroutineContext[ContinuationInterceptor]!!
.javaClass
.name.contains("ExecutorCoroutineDispatcher")
)
Assertions.assertTrue(Thread.currentThread().name.startsWith("pool"))
}
}
А вот Dispatchers.Unconfined — это указатель на объект Unconfined, в котором метод isDispatchNeeded всегда возвращает false:
override fun isDispatchNeeded(context: CoroutineContext): Boolean = false
Это означает, что корутина начнёт выполняться в потоке вызова, но после первой suspend-точки возобновится уже в другом потоке:
runBlocking {
launch(Dispatchers.Unconfined) {
Assertions.assertTrue(Thread.currentThread().name.startsWith("main"))
delay(10)
Assertions.assertTrue(!Thread.currentThread().name.startsWith("main"))
}
}
Такой подход подходит для корутин, которые:
- не нагружают CPU
- не работают с разделяемыми данными
5. Область корутины (Coroutine Scope)
Интерфейс CoroutineScope содержит одно единственное свойство — coroutineContext.
Корутины создаются через функции-строители:
- launch
- async
(обе — расширения CoroutineScope)
Эти функции принимают три параметра:
- context (необязательный) — если не задан, используется EmptyCoroutineContext
- coroutineStart (необязательный) — по умолчанию CoroutineStart.DEFAULT. Также доступны LAZY, ATOMIC, UNDISPATCHED
- suspend block — блок кода, исполняемый в корутине
Контекст новой корутины формируется путём объединения:
- текущего CoroutineScope.coroutineContext
- переданного context
- дополнительных элементов конфигурации
Затем создаётся корутинный объект — реализация AbstractCoroutine:
- для launch: StandaloneCoroutine или LazyStandaloneCoroutine
- для async: DeferredCoroutine или LazyDeferredCoroutine
В конструктор передаётся полный контекст, включающий Job:
public final override val context: CoroutineContext = parentContext + this
- GlobalScope — это синглтон CoroutineScope, но без привязки к Job и с EmptyCoroutineContext.
Хотя его использование не рекомендуется в продакшене, он подходит для корутин верхнего уровня и Dispatchers.Unconfined.
6. Заключение
В этой статье мы рассмотрели:
- что такое CoroutineContext
- как работают элементы контекста, включая Job, CoroutineName, ExceptionHandler
- как использовать и выбирать Dispatchers
- чем отличаются привязанные и непривязанные диспетчеры
- как устроена CoroutineScope и создание корутин
Эти знания помогут вам более эффективно управлять параллелизмом в Kotlin и использовать корутины безопасно и гибко.
Оригинал статьи: https://www.baeldung.com/kotlin/coroutine-context-dispatchers