Техники асинхронного программирования
Этот учебник объясняет различные подходы к асинхронному программированиюНа протяжении десятилетий разработчики сталкиваются с проблемой предотвращения блокировки своих приложений. Разрабатывая приложения для настольных компьютеров, мобильных устройств или серверов, мы хотим избежать ожидания пользователя, а ещё хуже – возникновения узких мест, которые препятствуют масштабированию приложения.
Существует множество подходов к решению этой проблемы, в том числе:
- Потоки
- Обратные вызовы
- Будущее, обещания и т. д.
- Реактивные расширения
- Корутины
Прежде чем объяснять, что такое корутины, давайте кратко рассмотрим некоторые другие решения.
Потоки
Потоки, пожалуй, наиболее известный подход для предотвращения блокировки приложений.
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