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
в исполняемом блоке кода корутины.