Spec-Zone.ru › Kotlin 1.8

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

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

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

  • Потоки

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

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

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

  • Корутины

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

Потоки

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

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"

Это подразумевает другой подход к решению задач и значительный сдвиг по сравнению с тем, как мы пишем синхронный код. Одним из преимуществ по сравнению с фьючерсами является то, что, поскольку Rx портирован на множество платформ, мы, как правило, можем рассчитывать на согласованный 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 они просто библиотечные функции.

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

Последнее изменение: 10 января 2023 г.
Это выражения Корутины

© 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

Spec-Zone.ru

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