Корутины Kotlin предоставили Android разработчикам модификатор suspend. Изучив его, вы поймете, почему функция suspend не возвращает ничего до тех пор, пока не будет завершена вся начатая работа, и как код может приостановить работу без блокировки потоков.
TL; DR; компилятор Kotlin создаст конечный автомат для каждой функции suspend, которая управляет выполнением корутины за нас!
Корутины упрощают асинхронные операции на Android. Как поясняется в документации, мы можем использовать их для управления асинхронными задачами, которые в противном случае могут заблокировать основной поток и привести к зависанию приложения.
Корутины также полезны для замены API, основанных на обратном вызове, на императивно выглядящий код. Например, взгляните на этот асинхронный код, который использует обратные вызовы:
// Упрощенный код, который рассматривает только успешную работу
fun loginUser(userId: String, password: String, userResult: Callback<User>) {
// Асинхронные обратные вызовы
userRemoteDataSource.logUserIn { user ->
// Успешный сетевой запрос
userLocalDataSource.logUserIn(user) { userDb ->
// Результат сохранен в БД
userResult.success(userDb)
}
}
}
Эти обратные вызовы могут быть преобразованы в последовательные вызовы функций с помощью корутин:
suspend fun loginUser(userId: String, password: String): User {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
return userDb
}
В коде корутин мы добавили модификатор функции suspend. Он говорит компилятору, что эта функция должна выполняться внутри корутины. Как разработчик, вы можете рассматривать suspend как обычную функцию, выполнение которой может быть приостановлено и возобновлено в какой-то момент.
В отличие от обратных вызовов, корутины обеспечивают простой способ переключения между потоками и обработкой исключений.
Но что же на самом деле делает компилятор под капотом, когда мы помечаем функцию как suspend?
Возвращаясь к функции suspend loginUser
, обратите внимание, что другие функции, которые она вызывает, также являются функциями suspend:
suspend fun loginUser(userId: String, password: String): User {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
return userDb
}
// UserRemoteDataSource.kt
suspend fun logUserIn(userId: String, password: String): User
// UserLocalDataSource.kt
suspend fun logUserIn(userId: String): UserDb
Если кратко, то компилятор Kotlin берет функции suspend и преобразует их в оптимизированную версию обратных вызовов с использованием конечного автомата, о котором мы поговорим позже.
Вы правильно поняли, компилятор напишет эти обратные вызовы за вас!
Таким образом, функции suspend взаимодействуют друг с другом с помощью объектов Continuation
. Continuation
— это просто общий интерфейс обратного вызова с некоторой дополнительной информацией. Как мы увидим позже, он будет представлять собой сгенерированный конечный автомат функции suspend.
Давайте взглянем на его определение:
interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(value: Result<T>)
}
context
будет являться CoroutineContext
, который будет использоваться в этом продолжении (continuation).resumeWith
возобновляет выполнение корутины с Result
, который может содержать либо значение, являющееся результатом вычисления, вызвавшего приостановку, либо исключение.Примечание: начиная с Kotlin 1.3 и далее, вы также можете использовать функции расширения resume(value: T)
и resumeWithException(exception: Throwable)
, которые являются специализированными версиями вызова resumeWith
.
Компилятор заменит модификатор suspend дополнительным параметром completion
(типа Continuation
) в сигнатуре функции, которая будет использоваться для передачи результата функции suspend вызвавшей ее корутине:
fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
completion.resume(userDb)
}
Наш пример вернет Unit
вместо User
. Объект User
будет “возвращен” в добавленном параметре Continuation
.
Байт-код функции suspend фактически возвращает Any?
, потому что это тип объединения T | COROUTINE_SUSPENDED
. Это позволяет функции по возможности возвращаться синхронно.
Примечание: если вы пометите функцию, которая не вызывает другие функции suspend с этим модификатором, компилятор добавит дополнительный параметр Continuation, но ничего с ним не сделает, байт-код тела функции будет выглядеть как обычная функция.
Вы также можете увидеть интерфейс Continuation
в других местах:
• При преобразовании API-интерфейсов на основе обратного вызова в корутины с использованием suspendCoroutine
или suspendCancellableCoroutine
(предпочтительнее) вы непосредственно взаимодействуете с объектом Continuation
, чтобы возобновить корутину, которая была приостановлена после выполнения блока кода, переданного в качестве параметра.
• Вы можете запустить корутину с помощью функции расширения startCoroutine
для функции suspend. Он принимает объект Continuation
в качестве параметра, который будет вызван, когда новая корутина завершится либо результатом, либо исключением.
Вы можете переключаться между различными диспетчерами для выполнения вычислений в разных потоках. Откуда Kotlin знает, где возобновить приостановленные вычисления?
Существует подтип Continuation
, называемый DispatchedContinuation
, функция resume которого делает вызов Dispatcher
доступным в CoroutineContext
. Все диспетчеры будут вызывать отправку, кроме Dispatchers.Unconfined
, чье переопределение функции isDispatchNeeded
(которая вызывается перед dispatch
) всегда возвращает false
.
Внимание: код, который будет показан в остальной части статьи, не будет полностью соответствовать байт-коду, сгенерированному компилятором. Это будет код Kotlin, достаточно точный, чтобы позволить вам понять, что на самом деле происходит внутри. Это представление генерируется корутинами версии 1.3.3 и может измениться в будущих версиях библиотеки.
Компилятор Kotlin определит, когда функция может приостановить работу внутри системы. Каждая точка приостановки будет представлена как состояние в конечном автомате. Они представляются компилятором с помощью меток:
fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {
// Label 0 -> первое выполнение
val user = userRemoteDataSource.logUserIn(userId, password)
// Label 1 -> продолжает с userRemoteDataSource
val userDb = userLocalDataSource.logUserIn(user)
// Label 2 -> продолжает с userLocalDataSource
completion.resume(userDb)
}
Для лучшего представления конечного автомата компилятор будет использовать оператор when
для реализации различных состояний:
fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {
when(label) {
0 -> { // Label 0 -> первое выполнение
userRemoteDataSource.logUserIn(userId, password)
}
1 -> { // Label 1 -> продолжает с userRemoteDataSource
userLocalDataSource.logUserIn(user)
}
2 -> { // Label 2 -> продолжает с userLocalDataSource
completion.resume(userDb)
}
else -> throw IllegalStateException(...)
}
}
Этот код является неполным, поскольку различные состояния не имеют возможности обмениваться информацией. Для этого компилятор будет использовать тот же объект Continuation
в функции. Вот почему исходный Continuation
является Any?
вместо возвращаемого типа исходной функции (т. е. User
).
Кроме того, компилятор создаст закрытый класс, который 1) содержит необходимые данные и 2) рекурсивно вызывает функцию loginUser
для возобновления выполнения. Вы можете проверить приблизительную реализацию сгенерированного класса ниже.
Внимание: комментарии не генерируются компилятором. Я добавил их, чтобы объяснить, что выполняется в коде.
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
class LoginUserStateMachine(
// параметр completion обратный вызов к функции
// которая вызвала loginUser
completion: Continuation<Any?>
): CoroutineImpl(completion) { // Локальные переменные функции suspend
var user: User? = null
var userDb: UserDb? = null // Общие объекты во всех CoroutineImpls
var result: Any? = null
var label: Int = 0 // эта функции вызывает loginUser снова для вызова триггера
// конечного автомата (метка уже будет находится в следующем состоянии) и
// результат будет результатом вычисления предыдущего состояния
override fun invokeSuspend(result: Any?)
{
this.result = result
loginUser(null, null, this)
}
}
...
}
Поскольку invokeSuspend
снова вызовет loginUser
только с информацией объекта Continuation
, остальные параметры в сигнатуре функции loginUser
становятся недействительными. На этом этапе компилятору просто нужно добавить информацию о том, как перемещаться между состояниями.
Первое, что ему нужно сделать, это знать, если 1) это первый раз, когда функция вызывается или 2) функция возобновилась из предыдущего состояния. Так он проверяет, имеет ли переданное continuation тип LoginUserStateMachine
или нет:
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
...
val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
...
}
Если это происходит в первый раз, он создаст новый экземпляр LoginUserStateMachine
и сохранит полученный экземпляр completion
в качестве параметра, чтобы он помнил, как возобновить функцию, вызвавшую его. Если это не так, то он просто продолжит выполнение конечного автомата (функция suspend).
Теперь давайте рассмотрим код, который компилятор генерирует для перемещения между состояниями и обмена информацией между ними:
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
...
val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
when(continuation.label) {
0 -> {
// Проверка на ошибки
throwOnFailure(continuation.result)
// В следующий раз вызова continuation он должен перейти в состояние 1
continuation.label = 1
// Объект continuation передан в logUserIn для продолжения
// выполнения этого конечного автомата, когда он закончится
userRemoteDataSource.logUserIn(userId!!, password!!, continuation)
}
1 -> {
// Проверка на ошибки
throwOnFailure(continuation.result)
// Получить результат предыдущего состояния
continuation.user = continuation.result as User
// В следующий раз вызова continuation он должен перейти в состояние 1
continuation.label = 2
// Объект continuation передан в logUserIn для продолжения
// выполнения этого конечного автомата, когда он закончится
userLocalDataSource.logUserIn(continuation.user, continuation)
}
... // оставляем последнее состояние на всякий случай
}
}
Сравните код выше с предыдущими фрагментами. Давайте посмотрим, что генерирует компилятор:
when
является label
из экземпляра LoginUserStateMachine
.logUserIn
) label
экземпляра LoginUserStateMachine
обновляется до следующего состояния.continuation
(типа LoginUserStateMachine
) передается в качестве параметра. Вызываемая функция suspend также была преобразована компилятором, и это еще один конечный автомат, подобный тому, который принимает объект continuation в качестве параметра! Когда конечный автомат этой функции suspend завершится, она возобновит его выполнение.Последнее состояние отличается тем, что оно должно возобновить выполнение функции, которая вызвала эту функцию. Как видно из кода, она вызывает resume для переменной cont
, хранящейся (во время построения) в LoginUserStateMachine
:
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
...
val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
when(continuation.label) {
...
2 -> {
// Проверка на ошибки
throwOnFailure(continuation.result)
// Получить результат предыдущего состояния
continuation.userDb = continuation.result as UserDb
// Продолжает выполнение функции, которая вызвала эту
continuation.cont.resume(continuation.userDb)
}
else -> throw IllegalStateException(...)
}
}
Компилятор Kotlin делает для нас очень много. Из этой функции suspend:
suspend fun loginUser(userId: String, password: String): User {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
return userDb
}
Компилятор сгенерировал:
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
class LoginUserStateMachine(
// параметр continuation - это обратный вызов функции, которая вызвала loginUser
completion: Continuation<Any?>
): CoroutineImpl(completion) {
// объекты для хранения в функции suspend
var user: User? = null
var userDb: UserDb? = null
// Общие объекты для всех CoroutineImpl
var result: Any? = null
var label: Int = 0
// Эта функция вызывает loginUser снова для вызова триггера
// конечного автомата (метка уже будет находиться в следующем состоянии) и
// результат будет результатом вычисления предыдущего состояния
override fun invokeSuspend(result: Any?) {
this.result = result
loginUser(null, null, this)
}
}
val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
when(continuation.label) {
0 -> {
// Проверка на ошибки
throwOnFailure(continuation.result)
// В следующий раз вызова continuation он должен перейти в состояние 1
continuation.label = 1
// Объект continuation передан в logUserIn для продолжения
// выполнения этого конечного автомата, когда он закончится
userRemoteDataSource.logUserIn(userId!!, password!!, continuation)
}
1 -> {
// Проверка на ошибки
throwOnFailure(continuation.result)
// Получение результата предыдущего состояния
continuation.user = continuation.result as User
// В следующий раз вызова continuation он должен перейти в состояние 2
continuation.label = 2
// Объект continuation передан в logUserIn для продолжения
// выполнения этого конечного автомата, когда он закончится
userLocalDataSource.logUserIn(continuation.user, continuation)
}
2 -> {
// Проверка на ошибки
throwOnFailure(continuation.result)
// Получение результата предыдущего состояния
continuation.userDb = continuation.result as UserDb
// Продолжает выполнение функции, которая вызвала эту
continuation.cont.resume(continuation.userDb)
}
else -> throw IllegalStateException(...)
}
}
Зная, что делает компилятор под капотом, вы можете лучше понять, почему функция suspend ничего не вернет, пока не будет завершена вся начатая работа. Кроме того, как код может приостановить работу без блокировки потоков: информация о том, что должно быть выполнено при возобновлении функции, хранится в объекте Continuation
!
Перевод статьи Benoit: Why and How I Switched from Ruby to Python
Комментарии