
В прошлой статье разбирались с проблематикой, которую решает Kotlin. Теперь разберем какие идеи заложены при реализации асинхронности в Kotlin.
Вспомним ещё раз чем отличается настоящий параллелизм (parallel) от конкурентного(concurrent):
При настоящем параллелизме задачи выполняются на разных потоках, управляемых планировщиком операционной системой.
При конкурентном параллелизме общая задача разбивается на небольшие блоки-задания, управление выполнением которых берет на себя планировщик среды исполнения (не OS!!), который сам является частью программы. Блоки-задания, относящиеся к одной задаче могут выполнится на разных потоках операционной системы.
Structured Concurrency
В основе механизма управления корутин в Kotlin лежит подход, с подачи Мартина Сустрика получивший название — парадигма программирования Structured Concurrency (*).
Согласно парадигме Structured Concurrency:
-
рабочие задания упорядочиваются в древовидную структуру по отношению родитель — потомок (для Kotlin — рабочие задания это корутины)
-
жизненный цикл дочернего задания строго связан с жизненным циклом родительского задания таким образом, что:
-
родительское задание ожидает завершения (успешного или с ошибкой) всех дочерних заданий
-
отмена родительского задания приводит к завершению всех дочерних заданий
-
ошибка при выполнении дочернего задания пробрасывается в родительское задание
-
* идею витали в воздухе и к моменту, когда Мартин Сустрик сформулировал свой принцип, разработчики Kotlin уже пришли к аналогичному решению и даже его реализовали в корутинах. Но само название «Structured Concurrency» оказалось удачным и закрепилось в документации.
CoroutineContext
В основе реализации механизма корутин лежит интерфейс CoroutineContext и набор базовых реализаций этого интерфейса (AbstractCoroutineContextElement, EmptyCoroutineContext, CombinedContext).
Все вместе (интерфейс CoroutineContext и базовые реализации), с одной стороны, реализуют паттерн компоновщик, а с другой, благодаря возможностям Kotlin, позволяют работать с контекстом и объектами, которые в нем лежат через операторы ‘+’, ‘[ ]‘.
Если ваши классы реализуют интерфейс CoroutineContext.Element, то можно использовать CoroutineContext для компоновки собственных элементов в свой отдельный контекст, не привязываясь к контексту корутин.
class AnyContextElement(val name: String) : CoroutineContext.Element { override val key: CoroutineContext.Key<*> get() = Key companion object Key : CoroutineContext.Key<AnyContextElement>}class OtherContextElement : CoroutineContext.Element { override val key: CoroutineContext.Key<*> get() = Key companion object Key : CoroutineContext.Key<OtherContextElement>}// Создаем собственный контекст с элементом AnyContextElement("name1") var customContext = EmptyCoroutineContext + AnyContextElement("name1")println(customContext[AnyContextElement]?.name) // Вывод: name1// Добавили к контексту ещё одни элементcustomContext += OtherContextElement()// Заменили в контексте AnyContextElement другим объектомcustomContext += AnyContextElement("name2")println(customContext[AnyContextElement]?.name) // Вывод: name2
CoroutineScope
CoroutineScope является техническим интерфейсом, хранящем в своем контексте объекты, определяющую работу корутин.
public interface CoroutineScope { public val coroutineContext: CoroutineContext}public fun CoroutineScope(context: CoroutineContext): CoroutineScope = ContextScope(if (context[Job] != null) context else context + Job())
CoroutineScope интересен двумя вещами:
-
содержит coroutineContext, который объединяет объекты, определяющие поведение корутин (но не только), в частности, хотя бы объект типа Job
-
содержит extension-функции для запуска дочерних корутин — launch и async
Текущий контекст можно получить через вызов функции currentCoroutineContext().
Любая корутина может быть запущена только в рамках какого-то CoroutineScope.
Точкой входа в механизм корутин является функция runBlocking.
fun main() = runBlocking { // объект this указывает на текущий CoroutineScope и можно вызывать // extension-функции launch { ... } }
При создании новой корутины создается новый coroutineContext на базе родительского coroutineContext. При этом создается новый объект Job, дочерний по отношению к Job родительской корутины. (**)
val scope = CoroutineScope(EmptyCoroutineContext)val job1 = scope.launch { ... }val job2 = scope.launch { ... }val job3 = scope.launch { ... }

** в runtime объект, доступный через вызов coroutineContext[Job], и есть текущая корутина; конкретный класс объекта корутины наследуется от класса AbstractCoroutine, реализующего интерфейсы Job и CoroutineScope
Объект Job в coroutineContext обеспечивает реализацию механизма Structured Concurrency и, в частности, позволяет останавливать выполнение дочерних корутин.
val scope = CoroutineScope(EmptyCoroutineContext)val job1 = scope.launch { ... }val job2 = scope.launch { ... }val job3 = scope.launch { ... }scope.cancel() // Вызывается scope.coroutineContext[Job].cancel()
Родительский Job не будет завершен пока не выполнятся все дочерние корутины.
Важно понимать что принцип Structured Concurrency относится к корутинам, запущенным в рамках одной иерархии корутин (одного сoroutineContext).
Structured Concurrency на примерах
Пример 1
// runBlocking - точка входа в мир корутин; создает свой scoperunBlocking { // this - ссылка на scope runBlocking // корутины запускаются в scope runBlocking launch { while(true) { delay(2000) } } // this.launch launch { while(true) { delay(3000) } } launch { while(true) { delay(3000) } } // ВИСИМ !!!}
Корутины, запускаемые через launch корутины являются дочерними по отношению к корутине выполняющей lambda-функцию, переданную в runBlocking. Родительская корутина ожидает завершения работы дочерних корутин.
Пример 2
runBlocking { val scope = CoroutineScope(EmptyCoroutineContext) scope.launch { while(true) { delay(2000) } } scope.launch { while(true) { delay(3000) } } scope.launch { while(true) { delay(3000) } } // выходим из runBlocking не дожидаясь завершения корутин}
В данном примере в методе runBlocking создается новый scope. Новый scope и scope runBlocking не связаны между собой.
Пример 3
runBlocking { val scope = CoroutineScope(EmptyCoroutineContext) + coroutineContext scope.launch { while(true) { delay(2000) } } scope.launch { while(true) { delay(3000) } } scope.launch { while(true) { delay(3000) } } // ВИСИМ!!!}
В примере 3 несмотря на то, что создается новый scope, используется общий с runBlocking контекст. Корутина, выполняемая в runBlocking является родительской по отношению к корутинам запущенным через scope.launch и ожидает завершения дочерних корутин.
Пример 4
val scope1 = CoroutineScope(EmptyCoroutineContext)val job = scope1.launch { val scope2 = CoroutineScope(EmptyCoroutineContext) val scope2Job = scope2.launch { ... } val job1 = launch { ... } val job2 = launch { ... } val job3 = launch { ... }}job.cancel()
В примере 4 при вызове job.cancel будут остановлены корутины job1, job2, job3; Корутина scope2Job продолжит работу.
Пример 5
runBlocking { doFn() // запускается на scope runBlocking // Висим!!!}suspend fun doFn() { val currentJob = currentCoroutineContext()[Job] (currentJob as? CoroutineScope)?.launch { while(true) { delay(2000) } }}
Пример 5 демонстрирует, что объект, представляющий текущую корутину можно получить через контекст и то, что конкретный класс объекта корутины реализует интерфейс CoroutineScope и связан с контекстом runBlocking.
Пример 6
runBlocking { launch { doFn() } // ВИСИМ !!!}suspend fun doFn() = withContext(EmptyCoroutineContext) { launch { while(true) { delay(2000) } }}
В примере 6 несмотря на то, что в withContext передается новый контекст(EmptyCoroutineContext), но в реализации withContext контексты суммируются и полученная в результате корутина принадлежит иерархии корутин runBlocking.
Пример 7
suspend fun doFn() = withContext(Job()) { launch { while(true) { delay(2000) } }}
Исходя из вышеизложенного становится понятно почему при попытке передать в builder-функцию объект Job или контекст, содержаший Job можно увидеть предупреждение:
Passing ‘CoroutineContext’ with a ‘Job’ to ‘withContext’ builder can lead to structured concurrency violations
Пример 8 — демонстрирует доступ к иерархии корутин
scope.coroutineContext[Job]?.children?.forEach { job -> // launched job println(job) // grandchildren job.children.forEach { println(job) }}
Заключение
В основе механизма управления корутин лежит принцип получивший название Structured Concurrency.
Применительно к Кotlin подход реализуется следующим образом:
-
корутины упорядочиваются в древовидную иерархию по отношению родитель-потомок
-
родительская корутина дожидается завершения дочерних корутин
-
остановка родительской корутины приводит к остановке дочерних корутин
-
за хранение объектов, определяющий работу корутин отвечает контекст корутины — coroutineContext
-
текущий контекст можно получить через метод currentCoroutineContext()
-
управлять корутинами можно через объект типа Job, который всегда есть в контексте корутины и который можно получить coroutineContext[Job]
-
при создании корутины происходит объединение контекстов — контекста родительской корутины и дополнительного контекста, созданного на основе параметров, переданных в builder-функцию
-
корректная иерархия корутин может быть выстроена только если при создании дочерней корутины дополнительный контекст не содержит элемент Job; в контексте дочерней корутины будет содержаться Job, дочерний по отношению к Job родительского контекста
Ссылки и благодарности
ссылка на оригинал статьи https://habr.com/ru/articles/1067510/