Spec-Zone.ru › Kotlin 1.6

Асинхронные методы программирования

На протяжении десятилетий разработчики сталкиваются с задачей — как предотвратить блокирование приложений. Будь то разработка настольных, мобильных или серверных приложений, мы хотим избежать ожидания пользователем или, что еще хуже, возникновения узких мест, которые мешают масштабированию приложения.

Существует множество подходов к решению этой проблемы, включая:

  • Потоки

  • Обратные вызовы

  • Фьючерсы, промисы и другие

  • Реактивные расширения

  • Корутины

Прежде чем объяснить, что такое корутины, давайте кратко рассмотрим некоторые другие решения.

Потоки

Потоки, пожалуй, являются наиболее известным подходом для предотвращения блокировки приложений.

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, но даже там разработчики обычно перешли к другим подходам, таким как промисы или реактивные расширения.

Фьючерсы, промисы и другие

Идея, стоящая за фьючерсами или промисами (в зависимости от языка/платформы, есть и другие термины), заключается в том, что при выполнении вызова нам гарантируется, что в какой-то момент он вернет объект, называемый Promise, над которым можно затем выполнить операции.

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# компанией Erik Meijer. Хотя они определенно использовались на платформе .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 же это просто функции библиотеки.

Для получения дополнительной информации см. справочник по корутинам.

Последнее изменение: 07 апреля 2022 г.
Выражения this Корутины

© 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API