Kotlin

Coroutine Context и Dispatcher

В этом посте мы поговорим о CoroutineContext и о диспатчерах, как о важной его части.

Coroutine Context

У каждой корутины есть связанный с ней CoroutineContext, который является хранилищем элементов (CoroutineContext.Element). Это хранилище похоже на Map. Каждый элемент доступен по своему уникальному ключу, что обеспечивает безопасность типов (type-safety).

public operator fun <E : Element> get(key: Key<E>): E?

Все корутины реализуют интерфейс CoroutineScope и имеют свойство coroutineContext. Поэтому мы можем обращаться к нему внутри блока корутины:

runBlocking {
   assertNotNull(coroutineContext)
}

А также можем получать из него элементы:

runBlocking {
   assertNotNull(coroutineContext[Job])
}

Как изменить CoroutineContext

CoroutineContext неизменяемый (immutable), но мы можем создать новый контекст добавив или удалив элемент из контекста, либо объединив два контекста. Так же есть контекст, который не содержит элементов - EmptyCoroutineContext.

Мы можем объединить два контекста используя оператор plus (+). Каждый элемент сам по себе является контекстом, так как реализует интерфейс CoroutineContext. Поэтому мы можем создать новый контекст, добавив элемент к существующему контексту:

val context = EmptyCoroutineContext
val newContext = context + CoroutineName("AppCoder")
assertNotEquals(context, newContext)
assertEquals("AppCoder", newContext[CoroutineName]!!.name)

Или мы можем удалить элемент из контекста методом CoroutineContext.minusKey:

val context = CoroutineName("AppCoder")
val newContext = context.minusKey(CoroutineName)
assertNull(newContext[CoroutineName])
assertEquals(EmptyCoroutineContext, newContext)

Элементы CoroutineContext

Kotlin предлагает несколько реализаций CoroutineContext.Element для разных задач:

  • Отладка: CoroutineName, CoroutineId
  • Управление жизненным циклом: Job
  • Обработка исключений: CoroutineExceptionHandler
  • Управление потоками: ContinuationInterceptor и его реализация CoroutineDispatcher

Диспатчеры

CoroutineDispatcher наследуется от ContinuationInterceptor, который тоже является элементом контекста. Он определяет поток, на котором будет выполняться корутина.

Когда Kotlin выполняет корутину, он сперва проверяет что возвращает CoroutineDispatcher.isDispatchNeeded. Если true, то CoroutineDispatcher.dispatch назначает поток для ее выполнения. Если false, то корутина продолжает выполняться на текущем потоке (unconfined).

Существуют несколько реализаций CoroutineDispatcher. В библиотеке корутин есть внутренние синглтоны: DefaultScheduler, DefaultIoScheduler и Unconfined.

Стандартные диспатчеры доступны через свойства объекта kotlinx.coroutines.Dispatchers:

  • Dispatchers.Default: ссылается на DefaultScheduler
  • Dispatchers.Main: инициализирует и возвращает диспатчер главного потока
  • Dispatchers.Unconfined: ссылается на Unconfined
  • Dispatchers.IO: ссылается на DefaultIoScheduler

Передадим диспатчер билдеру корутины:

runBlocking {
   launch(Dispatchers.IO) {
       println(coroutineContext[ContinuationInterceptor])
       // Dispatchers.IO
   }
}

Мы так же можем создать диспатчер из пула потоков (thread pool). Для этого создаем экземпляр ExecutorService и делаем из него диспатчер через экстеншен asCoroutineDispatcher():

runBlocking {
   launch(Executors.newSingleThreadExecutor().asCoroutineDispatcher()) {
       println(coroutineContext[ContinuationInterceptor])
       // java.util.concurrent.Executors$FinalizableDelegatedExecutorService@79819141
   }
}

Unconfined диспатчер

Для эксперимента сделаем функцию, которая будет делать фоновую работу в своем отдельном потоке:

suspend fun getData(): String =
   suspendCoroutine { continuation ->
       thread(name = "worker") {
           Thread.sleep(300L)
           continuation.resume("data")
       }
   }

По умолчанию, если мы не передаем диспатчер в билдер корутины, он наследуется из родительского CoroutineScope:

runBlocking(Executors.newSingleThreadExecutor().asCoroutineDispatcher()) {
   launch {
       println(Thread.currentThread().name)
       // pool-1-thread-1 @coroutine#2
       val data = getData()
       println(Thread.currentThread().name)
       // pool-1-thread-1 @coroutine#2
   }
}

Мы видим, что после выполнения getData() корутина продолжила выполняться на потоке нашего диспатчера.

Диспатчер Dispatchers.Unconfined ссылается на объект Unconfied, у которого метод isDispatchNeeded возвращает false:

override fun isDispatchNeeded(context: CoroutineContext): Boolean = false

Это приведет к тому, что корутина при возврате из getData() не сменит поток, а продолжит выполняться на текущем потоке:

runBlocking(Executors.newSingleThreadExecutor().asCoroutineDispatcher()) {
   launch(Dispatchers.Unconfined) {
       println(Thread.currentThread().name)
       // pool-1-thread-1 @coroutine#2
       val data = getData()
       println(Thread.currentThread().name)
       // worker @coroutine#2
   }
}

Данное поведение подходит для корутин, которые не сильно нагружают процессор и не изменяют никаких общедоступных переменных.

Coroutine Scope

CoroutineScope - это интерфейс со всего одним свойством: coroutineContext. Тем не менее мы можем создавать корутины, используя функции билдеры корутин - экстеншены для CoroutineScope async и launch. Оба этих билдера принимают три аргумента:

  • context: если не передан, будет использован EmptyCoroutineContext
  • coroutineStart: если не передан, будет использован CoroutineStart.DEFAULT. Другие возможные варианты LAZY, ATOMIC и UNDISPATCHED.
  • suspend block: блок кода, исполняемый в корутине.

Для создания контекста новой корутины билдер добавляет значение аргумента context к текущему CoroutineScope.coroutineContext, а потом добавляет прочие элементы.

Затем билдер создает корутину как экземпляр (instance) одного из подклассов AbstractCoroutine:

  • launch: StandaloneCoroutine, или LazyStandaloneCoroutine
  • async: DeferredCoroutine, или LazyDeferredCoroutine

Билдер передает новый контекст в их конструктор. И уже суперкласс AbstractCoroutine добавляет к этому контексту себя как элемент Job.

public final override val context: CoroutineContext = parentContext + this

AbstractCoroutine так же реализует интерфейс CoroutineScope и доступен как this в исполняемом блоке кода корутины.

Теги: