Добавить в корзинуПозвонить
Найти в Дзене
Умный пульс

Контекст корутины и диспетчеры в Kotlin

В этом руководстве мы познакомимся с CoroutineContext, а затем подробно рассмотрим диспетчеры — один из ключевых элементов CoroutineContext. Корутины — это подпрограммы или процессы, поддерживающие кооперативную многозадачность. Это значит, что корутина может быть приостановлена, возобновлена или уступить управление другой корутине. В Kotlin ключевое слово suspend перед функцией означает, что функция приостанавливает вызывающего и может быть вызвана только внутри корутины. Существует несколько функций-строителей корутин: Каждая корутина имеет связанный с ней контекст — CoroutineContext, который представляет собой индексированное множество элементов (Elements). Что такое индексированное множество? Это гибрид множества и словаря: у каждого элемента есть уникальный ключ. Метод CoroutineContext.get позволяет безопасно по типу получить нужный элемент: public operator fun <E : Element> get(key: Key<E>): E? Все корутинные классы реализуют интерфейс CoroutineScope и имеют свойство coroutineC
Оглавление

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)

Эти функции принимают три параметра:

  1. context (необязательный) — если не задан, используется EmptyCoroutineContext
  2. coroutineStart (необязательный) — по умолчанию CoroutineStart.DEFAULT. Также доступны LAZY, ATOMIC, UNDISPATCHED
  3. 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