- PVSM.RU - https://www.pvsm.ru -

Остров Котлин
Предыдущие тексты этой серии: про AsyncTask [1], про Loaders [2], про Executors и EventBus [3], про RxJava [4].
Итак, этот час настал. Это статья, ради которой была написана вся серия: объяснение, как новый подход работает «под капотом». Если вы пока не знаете и того, как им пользоваться, вот для начала полезные ссылки:
А освоившись с корутинами, вы можете задаться вопросом, что позволило Kotlin предоставить эту возможность и как она работает. Прошу заметить, что здесь речь пойдёт только о стадии компиляции: про исполнение можно написать отдельную статью.
Первое, что нам нужно понять — в рантайме вообще-то не существует никаких корутин. Компилятор превращает функцию с модификатором suspend в функцию с параметром Continuation [8]. У этого интерфейса есть два метода:
abstract fun resume(value: T)
abstract fun resumeWithException(exception: Throwable)
Тип T — это тип возвращаемого значения вашей исходной suspend-функции. И вот что на самом деле происходит: эта функция выполняется в определённом потоке (терпение, до этого тоже доберёмся), и результат передаётся в resume-функцию того continuation, в контексте которого вызывалась suspend-функция. Если функция не получает результат и выбрасывает исключение, то вызывается resumeWithException, пробрасывая ошибку вызывавшему коду.
Хорошо, но откуда взялось continuation? Разумеется, из корутиновского builder! Давайте посмотрим на код, создающий любую корутину, к примеру, launch:
public actual fun launch(
context: CoroutineContext = DefaultDispatcher,
start: CoroutineStart = CoroutineStart.DEFAULT,
parent: Job? = null,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context, parent)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
Тут builder создаёт корутину — экземпляр класса AbstractCoroutine, который, в свою очередь, реализует интерфейс Continuation. Метод start принадлежит интерфейсу Job. Но найти определение метода start весьма затруднительно. Но мы можем зайти тут с другой стороны. Внимательный читатель уже заметил, что первый аргумент функции launch — это CoroutineContext, и по умолчанию ему присвоено значение DefaultDispatcher. «Диспетчеры» — это классы, управляющие исполнением корутин, так что они определённо важны для понимания происходящего. Давайте посмотрим на объявление DefaultDispatcher:
public actual val DefaultDispatcher: CoroutineDispatcher = CommonPool
Так что, по сути, это CommonPool, хотя java-доки и говорят нам, что это может измениться. А что такое CommonPool?
Это диспетчер корутин, использующий ForkJoinPool [9] в качестве реализации ExecutorService. Да, это так: в конечном счёте все ваши лямбда-корутины — это просто Runnable, попавшие в Executor с набором хитрых трансформаций. Но дьявол как всегда в мелочах.

Fork? Или join?
Судя по результатам опроса в моём твиттере, тут требуется вкратце объяснить, что представляет собой FJP :)
В первую очередь, ForkJoinPool — это современный executor, созданный для использования с параллельными стримами Java 8. Оригинальная задача была в эффективном параллелизме при работе со Stream API, что по сути означает разделение потоков для обработки части данных и последующее объединение, когда все данные обработы. Упрощая, представим, что у вас есть следующий код:
IntStream
.range(1, 1_000_000)
.parallel()
.sum()
Сумма такого стрима не будет вычислена в одном потоке, вместо этого ForkJoinPool рекурсивно разобьёт диапазон на части (сначала на две части по 500 000, затем каждую из них на 250 000, и так далее), посчитает сумму каждой части, и объединит результаты в единую сумму. Вот визуализация такого процесса:

Потоки разделяются для разных задач и вновь объединяются после завершения
Эффективность FJP основана на алгоритме «похищения работы»: когда у конкретного потока кончаются задачи, он отправляется в очереди других потоков пула и похищает их задачи. Для лучшего понимания можно посмотреть доклад [12] Алексея Шипилёва или проглядеть презентацию [13].
Отлично, мы поняли, что выполняет наши корутины! Но как они там оказываются?
Это происходит внутри метода CommonPool#dispatch:
_pool.execute(timeSource.trackTask(block))
Метод dispatch вызывается из метода resume (Value: T) в DispatchedContinuation. Звучит знакомо! Мы помним, что Continuation — это интерфейс, реализованный в AbstractCoroutine. Но как они связаны?
Трюк заключён внутри класса CoroutineDispatcher. Он реализует интерфейс ContinuationInterceptor следующим образом:
public actual override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
DispatchedContinuation(this, continuation)
Видите? Вы предоставляете в builder корутин простой блок. Вам не требуется реализовывать никакие интерфейсы, о которых вы знать ничего не хотите. Это всё берёт на себя библиотека корутин. Она
перехватывает исполнение, заменяет continuation на DispatchedContinuation, и отправляет его в executor, который гарантирует наиболее эффективное выполнение вашего кода.
Теперь единственное, с чем нам осталось разобраться — как dispatch вызывается из метода start. Давайте восполним этот пробел. Метод resume вызывается из startCoroutine в extension-функции блока:
public fun <R, T> (suspend R.() -> T).startCoroutine(
receiver: R,
completion: Continuation<T>
) {
createCoroutineUnchecked(receiver, completion).resume(Unit)
}
А startCoroutine, в свою очередь, вызывается оператором "()" в перечислении CoroutineStart. Ваш builder принимает его вторым параметром, и по умолчанию это CoroutineStart.DEFAULT. Вот и всё!
Вот по какой причине меня восхищает подход корутин: это не только эффектный синтаксис, но и гениальная реализация.
А тем, кто дочитал до конца, достаётся эксклюзив: видеозапись [14] моего доклада «Скрипач не нужен: отказываемся от RxJava в пользу корутин в Котлине» с конференции Mobius [15]. Наслаждайтесь :)
Автор: dzigoro
Источник [16]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android/284153
Ссылки в тексте:
[1] про AsyncTask: https://habr.com/company/epam_systems/blog/348894/
[2] про Loaders: https://habr.com/company/jugru/blog/350094/
[3] про Executors и EventBus: https://habr.com/company/jugru/blog/351166/
[4] про RxJava: https://habr.com/company/jugru/blog/353852/
[5] Страница на официальном сайте: https://kotlinlang.org/docs/reference/coroutines.html
[6] Блог-пост Android Coroutine Recipes: https://proandroiddev.com/android-coroutine-recipes-33467a4302e9
[7] Доклад Романа Елизарова: https://www.youtube.com/watch?v=HYhJmK9nKS4&t=1s
[8] Continuation: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines.experimental/-continuation/index.html
[9] ForkJoinPool: https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinPool.html
[10] #Android: https://twitter.com/hashtag/Android?src=hash&ref_src=twsrc%5Etfw
[11] May 3, 2018: https://twitter.com/vvsevolodovich/status/991950848996597760?ref_src=twsrc%5Etfw
[12] доклад: https://www.youtube.com/watch?v=t0dGLFtRR9c
[13] презентацию: https://shipilev.net/talks/jeeconf-May2013-forkjoin.pdf
[14] видеозапись: https://www.youtube.com/watch?v=2NzoSQmNas0
[15] Mobius: https://mobiusconf.com
[16] Источник: https://habr.com/post/415335/?utm_campaign=415335
Нажмите здесь для печати.