Spec-Zone.ru › Kotlin 1.4

Техники асинхронного программирования

Этот учебник объясняет различные подходы к асинхронному программированию

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

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

  • Потоки
  • Обратные вызовы
  • Будущее, обещания и т. д.
  • Реактивные расширения
  • Корутины

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

Потоки

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

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 an 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 – это просто паттерн наблюдатель с рядом расширений, которые позволяют нам работать с данными.

По своему подходу это очень похоже на Futures, но Future возвращает дискретный элемент, а Rx – поток. Однако, как и раньше, это также приводит к совершенно новому способу мышления о модели программирования, чётко сформулированному так:

"everything is a stream, and it's observable"

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

Spec-Zone.ru

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