Асинхронные методы программирования
Десятилетиями разработчики сталкиваются с проблемой предотвращения блокировки своих приложений. Разрабатывая приложения для настольных компьютеров, мобильных устройств или серверной части, мы хотим избежать задержек для пользователя или, что еще хуже, узких мест, препятствующих масштабированию приложения.
Для решения этой проблемы применялись различные подходы, включая:
Прежде чем объяснить, что такое корутины, давайте кратко рассмотрим некоторые другие решения.
Потоки
Потоки, пожалуй, являются наиболее известным подходом для предотвращения блокировок приложений.
fun postItem(item: Item) {
val token = preparePost()
val post = submitPost(token, item)
processPost(post)
}
fun preparePost(): Token {
// makes a request and consequently blocks the main thread
return token
}
Предположим, в приведенном выше коде preparePost — это длительная операция, которая, следовательно, заблокирует пользовательский интерфейс. Мы можем запустить ее в отдельном потоке. Это позволит избежать блокировки пользовательского интерфейса. Это очень распространенный метод, но он имеет ряд недостатков:
Потоки не бесплатны. Потоки требуют контекстных переключений, которые дорогостоящие.
Потоков не бесконечно. Количество потоков, которые могут быть запущены, ограничено операционной системой. В серверных приложениях это может привести к серьезной проблеме с производительностью.
Потоки не всегда доступны. Некоторые платформы, такие как JavaScript, даже не поддерживают потоки.
Работа с потоками не проста. Отладка потоков и предотвращение гонок — распространенные проблемы при программировании с использованием нескольких потоков.
Обратные вызовы
С помощью обратных вызовов идея заключается в передаче одной функции в качестве параметра другой функции, и вызов этой функции происходит после завершения процесса.
fun postItem(item: Item) {
preparePostAsync { token ->
submitPostAsync(token, item) { post ->
processPost(post)
}
}
}
fun preparePostAsync(callback: (Token) -> Unit) {
// make request and return immediately
// arrange callback to be invoked later
}
По сути, это кажется более элегантным решением, но у него есть и ряд недостатков:
Сложность вложенных обратных вызовов. Обычно функция, используемая в качестве обратного вызова, часто нуждается в собственном обратном вызове. Это приводит к цепочке вложенных обратных вызовов, что затрудняет понимание кода. Такой паттерн часто называют «рождественской елкой» (фигурные скобки представляют ветви дерева).
Обработка ошибок усложнена. Модель вложения усложняет обработку и распространение ошибок.
Обратные вызовы довольно распространены в архитектурах с циклом событий, таких как JavaScript, но даже там люди обычно перешли на другие подходы, такие как промисы или реактивные расширения.
Фьючерсы, промисы и другие
Идея фьючерсов или промисов (есть и другие термины в зависимости от языка/платформы) заключается в том, что при вызове нам гарантируется, что в какой-то момент он вернет объект, называемый промисом, на котором затем можно выполнять операции.
fun postItem(item: Item) {
preparePostAsync()
.thenCompose { token ->
submitPostAsync(token, item)
}
.thenAccept { post ->
processPost(post)
}
}
fun preparePostAsync(): Promise<Token> {
// makes request and returns a promise that is completed later
return promise
}
Этот подход требует внесения ряда изменений в наш подход к программированию, в частности:
Разная модель программирования. Подобно обратным вызовам, модель программирования переходит от сверху вниз императивного подхода к композиционной модели с цепочкой вызовов. Традиционные структуры программ, такие как циклы, обработка исключений и т. д., обычно больше не применимы в этой модели.
Разные API. Обычно требуется освоение совершенно нового API, например,
thenComposeилиthenAccept, который также может отличаться в зависимости от платформы.Конкретный тип возвращаемого значения. Тип возвращаемого значения переходит от фактических данных, которые нам нужны, к возвращению нового типа
Promise, который нужно проанализировать.Обработка ошибок может быть сложной. Распространение и цепочку ошибок не всегда просто реализовать.
Реактивные расширения
Реактивные расширения (Rx) были введены в C# компанией Эрик Майер. Хотя они действительно использовались на платформе .NET, они стали широко распространенными только после того, как Netflix перенесла их в Java, назвав RxJava. С тех пор были созданы многочисленные порты для различных платформ, включая JavaScript (RxJS).
Идея Rx заключается в переходе к тому, что называется observable streams, где мы теперь рассматриваем данные как потоки (бесконечное количество данных), которые можно наблюдать. Практически Rx — это просто шаблон наблюдатель с рядом расширений, которые позволяют нам работать с данными.
По подходу он довольно похож на фьючерсы, но фьючерс возвращает отдельный элемент, а Rx возвращает поток. Однако, как и раньше, он также вводит совершенно новый способ мышления о нашей модели программирования, изящно сформулированный как
"everything is a stream, and it's observable"
Это подразумевает другой подход к решению проблем и значительный переход от того, к чему мы привыкли при написании синхронного кода. Одно из преимуществ по сравнению с фьючерсами заключается в том, что, поскольку он портирован на множество платформ, мы обычно можем получить согласованный опыт использования API независимо от того, используем ли мы C#, Java, JavaScript или любой другой язык, где доступно Rx.
Кроме того, Rx предлагает более удобный подход к обработке ошибок.
Корутины
В Kotlin для работы с асинхронным кодом используются корутины — идея приостанавливаемых вычислений, то есть возможность функции приостановить свою работу на определенном этапе и возобновить ее позже.
Однако одним из преимуществ корутин является то, что с точки зрения разработчика написание неблокирующего кода практически такое же, как написание блокирующего кода. Модель программирования сама по себе не меняется.
Например, следующий код:
fun postItem(item: Item) {
launch {
val token = preparePost()
val post = submitPost(token, item)
processPost(post)
}
}
suspend fun preparePost(): Token {
// makes a request and suspends the coroutine
return suspendCoroutine { /* ... */ }
}
Этот код запустит длительную операцию без блокировки основного потока. preparePost называется suspendable function, поэтому ключевое слово suspend предшествует ему. Как указано выше, это означает, что функция выполнится, приостановит выполнение и возобновит его в какой-то момент времени.
Подпись функции остается абсолютно такой же. Единственное отличие — добавление
suspend. Тип возвращаемого значения, однако, — это тип, который мы хотим вернуть.Код по-прежнему написан так, как будто мы пишем синхронный код, сверху вниз, без необходимости в специальном синтаксисе, кроме использования функции
launch, которая фактически запускает корутину (рассмотрено в других руководствах).Модель программирования и API остаются неизменными. Мы можем продолжать использовать циклы, обработку исключений и т. д., и нет необходимости изучать набор новых API.
Корутины независимы от платформы. Будь то JVM, JavaScript или любая другая платформа, написанный код остается одинаковым. Под капотом компилятор позаботится об адаптации к каждой платформе.
Корутины — это не новая концепция, и уж точно не изобретение Kotlin. Они существуют уже много десятилетий и популярны в некоторых других языках программирования, таких как Go. Важно отметить, что реализация корутин в Kotlin в основном делегируется библиотекам. Фактически, кроме ключевого слова suspend, в язык не добавляются другие ключевые слова. Это несколько отличается от таких языков, как C#, где async и await являются частью синтаксиса. В Kotlin это просто функции библиотек.
Дополнительную информацию см. в справочнике по корутинам.
© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/async-programming.html