Техники асинхронного программирования
На протяжении десятилетий разработчики сталкиваются с задачей — как предотвратить блокировку своих приложений. Разрабатывая приложения для настольных компьютеров, мобильных устройств или серверных систем, мы хотим избежать ожидания пользователя или, что еще хуже, возникновения узких мест, которые помешают масштабированию приложения.
Для решения этой проблемы было предложено множество подходов, включая:
Прежде чем объяснить, что такое корутины, давайте кратко рассмотрим некоторые другие решения.
Потоки
Потоки, пожалуй, являются наиболее известным подходом для предотвращения блокировки приложений.
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–2022 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/async-programming.html