Недавно JetBrains анонсировала новую фичу, которая известна как coroutines (coroutines является экспериментальной функцией в Kotlin 1.1+). Кстати, jetbrains — это место, где можно найти настоящее сокровище, а именно: «intellij download».
Coroutines упрощает асинхронное программирование, пряча всю сложность внутри библиотек.
Эта идея зародилась в далёком 1967 году в языке программирования Simula. Поначалу методов было всего несколько: detach и resume (следовательно, они позволяли останавливать и запускать исполнение).
Тем не менее, идея была отброшена, когда появились потоки. С одной стороны, имеет смысл запускать асинхронную задачу в отдельном потоке. Однако в некоторых случаях, это решение не будет эффективным. Например, в таких:
- Оба потока и переключение между ними дорого обходятся;
- Создание тысяч потоков может существенно нагрузить систему;
- Ваш код должен использовать только один поток (JavaScript, например);
- Вы не хотите использовать потоки, потому что у вас много «mutable state»;
- Вы можете изменить пользовательский интерфейс только из его потока, поэтому легко ошибиться.
Вот что разработчики говорят о coroutines: «По сути, coroutines — это вычисления, которые можно приостановить, не блокируя поток».
«Не блокируя поток» — что это значит? Проще говоря, речь идёт о функции, которая может быть запущена, отсоединена и возобновлена с того же места. Coroutines — это новый, удобный способ выполнения неблокирующих асинхронных операций. Кроме того, создание coroutines — это более «лёгкая» операция, по сравнению с потоками.
Прерываемые функций
В coroutines главное — это прерывание функций.
suspend fun doSomething(foo: Foo): Bar { ... }
Отсоединённые функции могут принимать параметры и возвращаемые значения, также как обычные функции. Кроме того, они могут вызываться из coroutines и других прерываемых функций, а также из литералов функций, которые в них внедрены.
Мы помечаем методы используя новое ключевое слово «suspend». Функция, помеченная таким образом, может прервать выполнение coroutines. В то же время, это не блокирует поток.
Давайте посмотрим, как это работает на практике. Когда вы задаёте прерываемую функцию, компилятор модифицирует метод следующим образом:
Было:
suspend fun submitPost( token: Token, item: Item): Post {...}
Стало:
Object submitPost (Token token, Item item, Continuation<Post> cont){...}
Текущая галочка, которая хранится в интерфейсе, передаётся вызову метода. Как только метод прекращает свою работу, вызывается Callback, и всё управление переходит в state machine.
public interface Continuation <in T> { public val context: CorotineContext public fun resume(value: T) public fun resumeWithException(exception: Throwable) } switch(cont.label) { case 0: cont.label=1; firstMethod(cont); break; case 1: cont.label=2; secondMethod(cont); break; case 2: thirdMethod(cont); break; }
State machine ― это объект создаваемый компилятором. Он имеет метки, которые определяют этапы выполнения кода. Он передаёт себя как Callback ― интерфейс Continuation.
В настоящее время, у coroutines экспериментальный статус. Но это не означает, что их нельзя использовать в разработке. Экспериментальный статус показывает, что их библиотеки будут расширены. На самом деле это означает, что их библиотеки будут расширены и текущие проблемы будут в основном затрагивать разработчиков (им придётся поместить свой код со всеми coroutines в пакет experimental, чтобы обеспечить переход после релиза).
Чтобы начать использовать coroutines, необходимо добавить все необходимые зависимости:
apply plugin: 'kotlin' kotlin { experimental { coroutines 'enable' } } dependencies { ... compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.21" }
В принципе, это всё. Вы также можете обернуть свой сетевой запрос в coroutines:
suspend fun makeRequest(request: Request): Result { return suspendCoroutine { continuation -> httpRequest(request) { ex, result -> if (ex != null) continuation.resumeWithException(ex) else continuation.resume(result!!) } } } fun httpRequest(request: Request, cb: (Throwable?, Result? -> Unit)) {...}
SuspendCoroutine остановит текущую coroutine, для выполнения асинхронной операции. Результат операции будет отправлен с помощью continuation, что возобновит coroutine.
Тем не менее, эта низкоуровневая операция ― не более чем один запрос. С помощью coroutines можно писать асинхронный код асинхронным способом. Взгляните на следующий пример:
fun preparePost(): Token{ val request = composeTokenRequest() val result = makeRequest(request) return result.parseToken() }
Вам нужно указать одну важную вещь:
Это означает, что асинхронные операции будут выполняться последовательно и вам придётся намеренно указать необходимость параллелизма. Порядок выполнения должен быть чётко определён.
Мы описали логику и теперь готовы приступить к её исполнению на высшем уровне. Кстати, если вы забыли сделать coroutines builders, компилятору это не понравится. В результате вы увидите следующее:
Запуск — это билдер coroutine, который мгновенно возвращает вызов. Coroutine будет продолжать работать в фоновом режиме. В качестве параметра билдер принимает контекст, в котором будет выполняться coroutine. Обычно это CommonPool и UI.
Вы можете заметить небольшую уловку. Прерываемые функции можно использовать только из других прерываемых функций. Таким образом, у нас есть функция запуска, которая может быть использована как угодно, с помощью которой могут быть использованы прерываемые функции. Как это работает?
Coroutine builders это не ключевые слова. Это обычные функции, которые принимают лямбда-выражение, помеченное ключевым словом suspend. В этом-то и сложность.
Вы можете подумать, «ОК. Круто. Но что, черт возьми, такое эти ваши coroutine?» Самый простой способ представить, что такое coroutine — это сравнить её с «легковесным» потоком. Разница лишь в том, что coroutines намного «дешевле» потоков.
Что если вы хотите выполнять операции одновременно? Для этого вам понадобится использовать coroutine builder async.
Подытожим
В настоящее время coroutines представляют собой эффективный инструмент для бэкэнд-разработчиков, работающих с высоконагруженными системами. Другая классная фича — это возможность писать асинхронный код в синхронном стиле.
Главный вопрос: «Стоит ли мобильным разработчикам использовать coroutines вместо RX java?»
Основным недостатком coroutines является то, что они недостаточно удобны. RX java имеет гораздо более удобные операторы. Тем не менее, оба решения имеют довольно высокий барьер входа и требуют углублённого изучения для реализации сложных решений.
Кто знает, может быть, через какое-то время coroutines получит ещё более удобный набор функций и станет «must-have» решением для мобильных разработчиков. Время покажет.
Перевод статьи JetRuby Agency : Kotlin Coroutines on Android — Farewell RxJava?