- Корутины Kotlin: как работать асинхронно в Android
- Сравним корутины с потоком
- Корутины легкие и супербыстрые
- Как же корутина приостанавливает свою работу?
- Корутины Kotlin: как работать асинхронно в Android
- Сравним корутины с потоком
- Корутины легкие и супербыстрые
- Как же корутина приостанавливает свою работу?
- Сопрограммы
- Блокирование против приостановки
- Останавливаемые функции
- Aннотация @RestrictsSuspension
- Внутреннее функционирование сопрограмм
- Экспериментальный статус сопрограмм сменился на стабильный
- Стандартные API
- Низкий уровень API: kotlin.coroutines
- API генераторов в kotlin.coroutines
- Другие API высокого уровня: kotlinx.coroutines
Корутины Kotlin: как работать асинхронно в Android
May 6, 2020 · 6 min read
Kotlin предоставляет корутины, которые помогают писать асинхронный код синхронно. Android — это однопоточная платформа, и по умолчанию все работает на основном потоке (потоке UI). Когда настает время запускать операции, несвязанные с UI (например, сетевой вызов, работа с БД, I/O операции или прием задачи в любой момент), мы распределяем задачи по различным потокам и, если нужно, передаем результат обратно в поток UI.
Android имеет свои механизмы для выполн е ния задач в другом потоке, такие как AsyncTask, Handler, Services и т.д. Эти механизмы включают обратные вызовы, методы post и другие приемы для передачи результата между потоками, но было бы лучше, если бы можно было писать асинхронный код так же, как синхронный.
С корутиной код выглядит легче. Нам не нужно использовать обратный вызов, и следующая строка будет выполнена, как только придет ответ. Можно подумать, что вызов функции из основного потока заблокирует её к тому времени, как ответ вернется, но с корутиной все иначе. Она не будет блокировать поток Main или любой другой, но все еще может выполнять код синхронно. Подробнее.
Сравним корутины с потоком
В примере выше допустим, что мы создаем новый поток, и после завершения задачи передаем результат в поток UI. Для того, чтобы сделать это правильно, нужно понять и решить несколько проблем. Вот список некоторых:
1. Передача данных из одного потока в другой — это головная боль. Так еще и грязная. Нам постоянно нужно использовать обратные вызовы или какой-нибудь механизм уведомления.
2. Потоки стоят дорого. Их создание и остановка обходится дорого, включает в себя создание собственного стека. Потоки управляются ОС. Планировщик потоков добавляет дополнительную нагрузку.
3. Потоки блокируются. Если вы выполняете такую простую задачу, как задержка выполнения на секунду (Sleep), поток будет заблокирован и не может быть использован для другой операции.
4. Потоки не знают о жизненном цикле. Они не знают о компонентах Lifecycle (Activity, Fragment, ViewModel). Поток будет работать, даже если компонент UI будет уничтожен, что требует от нас разобраться с очисткой и утечкой памяти.
Как будет выглядеть ваш код с большим количеством потоков, Async и т.д.? Мы можем столкнуться с большим количеством обратных вызовов, методов обработки жизненного цикла, передач данных из одного места в другое, что затруднит их чтение. В целом, мы потратили бы больше времени на устранение проблем, а не на логику программы.
Отметим , что это не просто другой способ асинхронного программирования , это другая парадигма.
Корутины легкие и супербыстрые
Пусть код скажет за себя.
Я создам 10к потоков, что вообще нереалистично, но для понимания эффекта корутин пример очень наглядный:
Здесь каждый поток ожидает 1 мс. Запуск этой функции занял около 12,6 секунд. Теперь давайте создадим 100к корутин (в 10 раз больше) и увеличим задержку до 10 секунд (в 10000 раз больше). Не волнуйтесь про “runBlocking” или “launch” (конструкторах Coroutine).
14 секунд. Сама задержка составляет 10 секунд. Это очень быстро. Создание 100 тысяч потоков может занять целую вечность.
Если вы посмотрите на метод creating_10k_Thread(), то увидите, что существует задержка в 1 мс. Во время нее он заблокирован, т.е. ничего не может делать. Вы можете создать только определенное количество потоков в зависимости от количества ядер. Допустим, возможно создать до 8 потоков в системе. В примере мы запускаем цикл на 10000 раз. Первые 8 раз будут созданы 8 потоков, который будут работать параллельно. На 9-й итерации следующий поток не сможет быть создан, пока не будет доступного. На 1 мс поток блокируется. Затем создастся новый поток и по итогу блокировка на 1мс создает задержку. Общее время блокировки для метода составит 10000/ мс. А также будет использоваться планировщик потоков, что добавит дополнительной нагрузки.
Для creatingCoroutines() мы установили задержку в 10 сек. Корутина приостанавливается, не блокируется. Пока метод ждет 10 секунд до завершения, он может взять задачу и выполнить ее после задержки. Корутины управляются пользователем, а не ОС, что делает их быстрее. В цифрах, каждый поток имеет свой собственный стек, обычно размером 1 Мбайт. 64 Кбайт — это наименьший объем пространства стека, разрешенный для каждого потока в JVM, в то время как простая корутина в Kotlin занимает всего несколько десятков байт heap памяти.
Еще пример для лучшего понимания:
Во фрагменте 1 мы последовательно вызываем методы fun1 и fun2 в основном потоке. На 1 секунду поток будет заблокирован. Теперь рассмотрим пример с корутиной.
Во фрагменте 2 это выглядит так, как будто они работают параллельно, но это невозможно, так как оба метода выполняются одним потоком. Эти методы выполняются одновременно потому, что функция задержки не блокирует поток, она приостанавливает его. И теперь, не теряя времени, этот же поток начинает выполнять следующую задачу и возвращается к ней, как только другая приостановленная функция (задержки) вернется к нему.
Корутина может обеспечить высокий уровень параллелизма с небольшими нагрузками. Несколько потоков также могут обеспечить параллелизм, но у них есть блокировка и переключение контекста. Корутина не блокирует, а приостанавливает поток для других задач. Большое количество корутин, выполняющих маленькие задачи, эффективнее, чем планировщик, поэтому тысячи корутин работают быстрее, чем десятки потоков.
Как же корутина приостанавливает свою работу?
Если вы посмотрите на выход, то увидите, что ‘completionHandler’ выполняется после завершения ‘asyncOperation’. ‘asyncOperation’ выполняется в фоновом потоке, а ‘completionHandler’ ожидает его завершения. В ‘completionHandler’ происходит обновление textview. Давайте рассмотрим байтовый код метода ‘asyncOperation’.
Во второй строке есть новый параметр под названием ‘continuation’, добавленный к методу asyncOperation. Continuation (продолжение) — это рабочий вариант для приостановки кода. Продолжение добавляется в качестве параметра к функции, если она имеет модификатор ‘suspend’. Также он сохраняет текущее состояние программы. Думайте о нем как о передаче остальной части кода (в данном случае метода completionHandler()) внутрь оболочки Continuation. После завершения текущей задачи выполнится блок продолжения. Поэтому каждый раз, когда вы создаете функцию suspend, вы добавляете в нее параметр продолжения, который обертывает остальную часть кода из той же корутины.
Coroutine очень хорошо работает с Livedata, Room, Retrofit и т.д. Еще один пример с корутиной:
Источник
Корутины Kotlin: как работать асинхронно в Android
Kotlin предоставляет корутины, которые помогают писать асинхронный код синхронно. Android — это однопоточная платформа, и по умолчанию все работает на основном потоке (потоке UI). Когда настает время запускать операции, несвязанные с UI (например, сетевой вызов, работа с БД, I/O операции или прием задачи в любой момент), мы распределяем задачи по различным потокам и, если нужно, передаем результат обратно в поток UI.
Android имеет свои механизмы для выполнения задач в другом потоке, такие как AsyncTask, Handler, Services и т.д. Эти механизмы включают обратные вызовы, методы post и другие приемы для передачи результата между потоками, но было бы лучше, если бы можно было писать асинхронный код так же, как синхронный.
С корутиной код выглядит легче. Нам не нужно использовать обратный вызов, и следующая строка будет выполнена, как только придет ответ. Можно подумать, что вызов функции из основного потока заблокирует её к тому времени, как ответ вернется, но с корутиной все иначе. Она не будет блокировать поток Main или любой другой, но все еще может выполнять код синхронно. Подробнее.
Сравним корутины с потоком
В примере выше допустим, что мы создаем новый поток, и после завершения задачи передаем результат в поток UI. Для того, чтобы сделать это правильно, нужно понять и решить несколько проблем. Вот список некоторых:
1. Передача данных из одного потока в другой — это головная боль. Так еще и грязная. Нам постоянно нужно использовать обратные вызовы или какой-нибудь механизм уведомления.
2. Потоки стоят дорого. Их создание и остановка обходится дорого, включает в себя создание собственного стека. Потоки управляются ОС. Планировщик потоков добавляет дополнительную нагрузку.
3. Потоки блокируются. Если вы выполняете такую простую задачу, как задержка выполнения на секунду (Sleep), поток будет заблокирован и не может быть использован для другой операции.
4. Потоки не знают о жизненном цикле. Они не знают о компонентах Lifecycle (Activity, Fragment, ViewModel). Поток будет работать, даже если компонент UI будет уничтожен, что требует от нас разобраться с очисткой и утечкой памяти.
Как будет выглядеть ваш код с большим количеством потоков, Async и т.д.? Мы можем столкнуться с большим количеством обратных вызовов, методов обработки жизненного цикла, передач данных из одного места в другое, что затруднит их чтение. В целом, мы потратили бы больше времени на устранение проблем, а не на логику программы.
Отметим, что это не просто другой способ асинхронного программирования , это другая парадигма.
Корутины легкие и супербыстрые
Пусть код скажет за себя.
Я создам 10к потоков, что вообще нереалистично, но для понимания эффекта корутин пример очень наглядный:
Здесь каждый поток ожидает 1 мс. Запуск этой функции занял около 12,6 секунд. Теперь давайте создадим 100к корутин (в 10 раз больше) и увеличим задержку до 10 секунд (в 10000 раз больше). Не волнуйтесь про “runBlocking” или “launch” (конструкторах Coroutine).
14 секунд. Сама задержка составляет 10 секунд. Это очень быстро. Создание 100 тысяч потоков может занять целую вечность.
Если вы посмотрите на метод creating_10k_Thread(), то увидите, что существует задержка в 1 мс. Во время нее он заблокирован, т.е. ничего не может делать. Вы можете создать только определенное количество потоков в зависимости от количества ядер. Допустим, возможно создать до 8 потоков в системе. В примере мы запускаем цикл на 10000 раз. Первые 8 раз будут созданы 8 потоков, который будут работать параллельно. На 9-й итерации следующий поток не сможет быть создан, пока не будет доступного. На 1 мс поток блокируется. Затем создастся новый поток и по итогу блокировка на 1мс создает задержку. Общее время блокировки для метода составит 10000/ мс. А также будет использоваться планировщик потоков, что добавит дополнительной нагрузки.
Для creatingCoroutines() мы установили задержку в 10 сек. Корутина приостанавливается, не блокируется. Пока метод ждет 10 секунд до завершения, он может взять задачу и выполнить ее после задержки. Корутины управляются пользователем, а не ОС, что делает их быстрее. В цифрах, каждый поток имеет свой собственный стек, обычно размером 1 Мбайт. 64 Кбайт — это наименьший объем пространства стека, разрешенный для каждого потока в JVM, в то время как простая корутина в Kotlin занимает всего несколько десятков байт heap памяти.
Еще пример для лучшего понимания:
Во фрагменте 1 мы последовательно вызываем методы fun1 и fun2 в основном потоке. На 1 секунду поток будет заблокирован. Теперь рассмотрим пример с корутиной.
Во фрагменте 2 это выглядит так, как будто они работают параллельно, но это невозможно, так как оба метода выполняются одним потоком. Эти методы выполняются одновременно потому, что функция задержки не блокирует поток, она приостанавливает его. И теперь, не теряя времени, этот же поток начинает выполнять следующую задачу и возвращается к ней, как только другая приостановленная функция (задержки) вернется к нему.
Корутина может обеспечить высокий уровень параллелизма с небольшими нагрузками. Несколько потоков также могут обеспечить параллелизм, но у них есть блокировка и переключение контекста. Корутина не блокирует, а приостанавливает поток для других задач. Большое количество корутин, выполняющих маленькие задачи, эффективнее, чем планировщик, поэтому тысячи корутин работают быстрее, чем десятки потоков.
Как же корутина приостанавливает свою работу?
Если вы посмотрите на выход, то увидите, что ‘completionHandler’ выполняется после завершения ‘asyncOperation’. ‘asyncOperation’ выполняется в фоновом потоке, а ‘completionHandler’ ожидает его завершения. В ‘completionHandler’ происходит обновление textview. Давайте рассмотрим байтовый код метода ‘asyncOperation’.
Во второй строке есть новый параметр под названием ‘continuation’, добавленный к методу asyncOperation. Continuation (продолжение) — это рабочий вариант для приостановки кода. Продолжение добавляется в качестве параметра к функции, если она имеет модификатор ‘suspend’. Также он сохраняет текущее состояние программы. Думайте о нем как о передаче остальной части кода (в данном случае метода completionHandler()) внутрь оболочки Continuation. После завершения текущей задачи выполнится блок продолжения. Поэтому каждый раз, когда вы создаете функцию suspend, вы добавляете в нее параметр продолжения, который обертывает остальную часть кода из той же корутины.
Источник
Сопрограммы
Сопрограммы получили статус стабильные в Kotlin 1.3. Детали см. ниже
Некоторые API инициируют долго протекающие операции (такие как сетевой ввод-вывод, файловый ввод-вывод, интенсивная обработка на CPU или GPU и др.), которые требуют блокировки вызывающего кода в ожидании завершения операций. Сопрограммы обеспечивают возможность избежать блокировки исполняющегося потока путём использования более дешёвой и управляемой операции: приостановки (suspend) сопрограммы.
Сопрограммы упрощают асинхронное программирование, оставив все осложнения внутри библиотек. Логика программы может быть выражена последовательно в сопрограммах, а базовая библиотека будет её реализовывать асинхронно для нас. Библиотека может обернуть соответствующие части кода пользователя в обратные вызовы (callbacks), подписывающиеся на соответствующие события, и диспетчировать исполнение на различные потоки (или даже на разные машины!). Код при этом останется столь же простой, как если бы исполнялся строго последовательно.
Многие асинхронные механизмы, доступные в других языках программирования, могут быть реализованы в качестве библиотек с помощью сопрограмм Kotlin. Это включает в себя async / await из C# и ECMAScript, channels и select из языка Go, и generators / yield из C# или Python. См. описания ниже о библиотеках, реализующих такие конструкции.
Блокирование против приостановки
Главным отличительным признаком сопрограмм является то, что они являются вычислениями, которые могут быть приостановлены без блокирования потока (вытеснения средствами операционной системы). Блокирование потоков часто является весьма дорогостоящим, особенно при интенсивных нагрузках: только относительно небольшое число потоков из общего числа является активно выполняющимися, поэтому блокировка одного из них ведет к затягиванию какой-нибудь важной части итоговой работы.
С другой стороны, приостановка сопрограммы обходится практически бесплатно. Не требуется переключения контекста (потоков) или иного вовлечения механизмов операционной системы. И сверх этого, приостановка может гибко контролироваться пользовательской библиотекой во многих аспектах: в качестве авторов библиотеки мы можем решать, что происходит при приостановке, и оптимизировать, журналировать или перехватывать в соответствии со своими потребностями.
Еще одно отличие заключается в том, что сопрограммы не могут быть приостановлены на произвольной инструкции, а только в так называемых точках остановки (приостановки), которые вызываются в специально маркируемых функциях.
Останавливаемые функции
Приостановка происходит в случае вызова функции, обозначенной специальным модификатором suspend :
Такие функции называются функциями остановки (приостановки), поскольку их вызовы могут приостановить выполнение сопрограммы (библиотека может принять решение продолжать работу без приостановки, если результат вызова уже доступен). Функции остановки могут иметь параметры и возвращать значения точно так же, как и все обычные функции, но они могут быть вызваны только из сопрограмм или других функций остановки. В конечном итоге, при старте сопрограммы она должна содержать как минимум одну функцию остановки, и функция эта обычно анонимная (лямбда-функция остановки). Давайте взглянем, для примера, на упрощённую функцию async() (из библиотеки kotlinx.coroutines ):
T`. So, when we pass a lambda to `async()`, it is a *suspending lambda*, and we can call a suspending function from it: —>
Здесь async() является обычной функцией (не функцией остановки), но параметр block имеет функциональный тип с модификатором suspend : suspend () -> T . Таким образом, когда мы передаём лямбда-функцию в async() , она является анонимной функцией остановки, и мы можем вызывать функцию остановки изнутри её:
Продолжая аналогию, await() может быть функцией остановки (также может вызываться из блока async <> ), которая приостанавливает сопрограмму до тех пор, пока некоторые вычисления не будут выполнены, и затем возвращает их результат:
Больше информации о том, как действительно работают функции async/await в kotlinx.coroutines , может быть найдено здесь.
Отметим, что функции приостановки await() и doSomething() не могут быть вызваны из обыкновенных функций, подобных main() :
Заметим, что функции остановки могут быть виртуальными, и при их переопределении модификатор suspend также должен быть указан:
Aннотация @RestrictsSuspension
Расширяющие функции (и анонимные функции) также могут быть маркированы как suspend , подобно и всем остальным (регулярным) функциям. Это позволяет создавать DSL и другие API, которые пользователь может расширять. В некоторых случаях автору библиотеки необходимо запретить пользователю добавлять новые пути приостановки сопрограммы.
Чтобы осуществить это, можно использовать аннотацию @RestrictsSuspension . Когда целевой класс или интерфейс R аннотируется подобным образом, все расширения приостановки должны делегироваться либо из членов R , либо из других его расширений. Поскольку расширения не могут делегировать друг друга до бесконечности (иначе программа никогда не завершится), гарантируется, что все приостановки пройдут посредством вызова члена R , так что автор библиотеки может полностью их контролировать.
Это актуально в тех редких случаях, когда каждая приостановка обрабатывается специальным образом в библиотеке. Например, при реализации генераторов через buildSequence() функцию, описанную ниже, мы должны быть уверены, что любой приостанавливаемый вызовов в сопрограмме завершается вызовом либо yield() , либо yieldAll() , а не какой-либо другой функции. Именно по этой причине SequenceBuilder аннотирована с @RestrictsSuspension:
Внутреннее функционирование сопрограмм
Мы не стремимся здесь дать полное объяснение того, как сопрограммы работают под капотом, но примерный смысл того, что происходит, очень важен.
Сопрограммы полностью реализованы с помощью технологии компиляции (поддержка от языковой виртуальной машины, среды исполнения, или операционной системы не требуется), а приостановка работает через преобразование кода. В принципе, каждая функция приостановки (оптимизации могут применяться, но мы не будем вдаваться в эти подробности здесь) преобразуется в конечный автомат, где состояния соответствуют приостановленным вызовам. Прямо перед приостановкой следующее состояние загружается в поле сгенерированного компилятором класса вместе с сопутствующими локальным переменными и т. д. При возобновлении сопрограммы локальные переменные и состояние восстанавливаются, и конечный автомат продолжает свою работу.
Приостановленную сопрограмму можно сохранять и передавать как объект, который хранит её приостановленное состояние и локальные переменные. Типом таких объектов является Continuation, а преобразование кода, описанное здесь, соответствует классическому Continuation-passing style. Следовательно, приостановливаемые функции принимают дополнительный параметр типа Continuation (сохранённое состояние) под капотом.
Более детально о том, как работают сопрограммы, можно узнать в этом проектном документе. Похожие описания async / await в других языках (таких как C# или ECMAScript 2016) актуальны и здесь, хотя особенности их языковых реализаций могут существенно отличаться от сопрограмм Kotlin.
Экспериментальный статус сопрограмм сменился на стабильный
Из-за былого экспериментального статуса сопрограмм все связанные API были собраны в стандартной библиотеке как пакет kotlin.coroutines.experimental . Дизайн стабилизирован и его экспериментальный статус снят, окончательный API перенесен в пакет kotlin.coroutines , а экспериментальный пакет хранится в целях обеспечения обратной совместимости.
Важное замечание: мы рекомендовали авторам библиотек, начавшим использовать эксперементальные сопрограммы следовать той же конвенции: добавить к названию суффикс «экспериментальный» (например, com.example.experimental ), указывающий, какой там используется сопрограммно совместимый API. Таким образом ваша библиотека сохранит бинарную совместимость. Сейчас, когда вышел финальный API-интерфейс, выполните следующие действия:
- скопируйте все API в com.example (без experimental суффикса);
- сохраните экспериментальный вариант пакета для обратной совместимости.
Это позволит минимизировать проблемы миграции для пользователей.
Поддержка экспериментальной версии сопрограмм будет прекращена в Kotlin 1.4
Стандартные API
Сопрограммы представлены в трёх их главных ингредиентах:
- языковая поддержка (функции остановки, как описывалось выше),
- низкоуровневый базовый API в стандартной библиотеке Kotlin,
- API высокого уровня, которые могут быть использованы непосредственно в пользовательском коде.
Низкий уровень API: kotlin.coroutines
Низкоуровневый API относительно мал и должен использоваться ТОЛЬКО для создания библиотек высокого уровня. Он содержит два главных пакета:
Более детальная информация о использовании этих API может быть найдена здесь.
API генераторов в kotlin.coroutines
Это функции исключительно «уровня приложения» в kotlin.coroutines :
Они перенесены в рамки kotlin-stdlib , поскольку они относятся к последовательностям. По сути, эти функции (и мы можем ограничиться здесь рассмотрением только sequence() ) реализуют генераторы, т. е. предоставляют лёгкую возможность построить ленивые последовательности:
Это сгенерирует ленивую, потенциально бесконечную последовательность Фибоначчи, используя сопрограмму, которая дает последовательные числа Фибоначчи, вызывая функцию yield (). При итерировании такой последовательности на каждом шаге итератор выполняет следующую часть сопрограммы, которая генерирует следующее число. Таким образом, мы можем взять любой конечный список чисел из этой последовательности, например fibonacciSeq.take(8).toList() , дающий в результате [1, 1, 2, 3, 5, 8, 13, 21] . И сопрограммы достаточно дёшевы, чтобы сделать это практичным.
Чтобы продемонстрировать реальную ленивость такой последовательности, давайте напечатаем некоторые отладочные результаты изнутри вызова sequence():
Запустите приведенный выше код, чтобы убедиться, что если мы будем печатать первые три элемента, цифры чередуются со STEP -ами по ветвям цикла. Это означает, что вычисления действительно ленивые. Для печати 1 мы выполняем только до первого yield(i) и печатаем START по ходу дела. Затем, для печати 2 , нам необходимо переходить к следующему yield(i) , и здесь печатать STEP . То же самое и для 3 . И следующий STEP никогда не будет напечатан (точно так же как и END ), поскольку мы никогда не запрашиваем дополнительных элементов последовательности.
Чтобы сразу породить всю коллекцию (или последовательность) значений, доступна функция yieldAll() :
Функция iterator() во всём подобна sequence(), но только возвращает ленивый итератор.
Вы могли бы добавить собственную логику выполнения функции sequence() , написав приостанавливаемое расширение класса SequenceScope (что порождается аннотацией @RestrictsSuspension , как описывалось выше):
Другие API высокого уровня: kotlinx.coroutines
Только базовые API, связанные с сопрограммами, доступны непосредственно из стандартной библиотеки Kotlin. Они преимущественно состоят из основных примитивов и интерфейсов, которые, вероятно, будут использоваться во всех библиотеках на основе сопрограмм.
Большинство API уровня приложений, основанные на сопрограммах, реализованы в отдельной библиотеке kotlinx.coroutines . Эта библиотека содержит в себе:
- Платформенно-зависимое асинхронное программирование с помощью kotlinx-coroutines-core :
- этот модуль включает Go-подобные каналы, которые поддерживают select и другие удачные примитивы
- исчерпывающее руководство по этой библиотеке доступно здесь.
- API, основанные на CompletableFuture из JDK 8: kotlinx-coroutines-jdk8
- Неблокирующий ввод-вывод (NIO), основанный на API из JDK 7 и выше: kotlinx-coroutines-nio
- Поддержка Swing ( kotlinx-coroutines-swing ) и JavaFx ( kotlinx-coroutines-javafx )
- Поддержка RxJava: kotlinx-coroutines-rx
Эти библиотеки являются удобными API, которые делают основные задачи простыми. Также они содержат законченные примеры того, как создавать библиотеки, построенные на сопрограммах.
Источник