Suspend kotlin что это
Корутины
Введение в корутины
В последнее время поддержка асинхронности и параллельных вычислений стала неотъемлимой чертой многих языков программирования. И Kotlin не является исключением. Зачем нужны асинхронность и параллельные вычисления? Параллельные вычисления позволяют выполнять несколько задач одновременно, а асинхронность позволяет не блокировать основной ход приложения во время выполнения задачи, которая занимает продолжительное время. Например, мы создаем графическое приложение для десктопа или мобильного устройства. И нам надо по нажатию на кнопку отправлять запрос к интернет-ресурсу. Однако подобный запрос может занять довольно много время. И чтобы приложение не зависало на период отправки запроса, подобные запросы к интернет-ресурсам следует отправлять асинхронно. При асинхронных запросах пользователь не ждет пока придет ответ от интернет-ресурса, а продолжает работу с приложением, а при получении ответа получит соответствующее уведомление.
Рассмотрим определение и применение корутины на простейшем примере.
Добавление kotlinx.coroutines
Прежде всего стоит отметить, что функциональность корутин (библиотека kotlinx.coroutines ) по умолчанию не включена в проект. И нам ее надо добавить. Если мы создаем проект консольного приложения в IntelliJ IDEA, то мы можем добавить соответствующую библиотеку в проект. Для этого в меню File перейдем к пункту Project Structure..
После добавления в проекте в узле External Libraries / KotlinJavaRuntime мы увидим добавленную библиотеку:
В других типах проектов для подключения kotlinx.coroutines может использоваться Gradle или другие способы. Так, если проект использует Gradle, то в файл gradle добавляется зависимость:
Определение suspend-функции
Сначала рассмотрим пример, который не использует корутины:
После выполнения работы цикла выводим на консоль строку «Hello Coroutines».
Если мы запустим приложение, то мы увидим следующий консольный вывод:
Здесь мы видим, что строка «Hello Coroutines» выводится после выполнения цикла. Но вместо цикла у нас могла бы быть более содержательная, но и более продолжительная работа, например, обращение к интернет-ресурсу, к удаленой базе данных, какие-то операции с файлами и т.д. И в этом случае все определенные после этой работы действия ожидали бы завершения этой продолжительной работы, как в данном случае строка «Hello Coroutines» ждет завершения цикла.
Определение корутины
и запускет эту корутину параллельно с остальным кодом. То есть данная корутина выполняется независимо от прочего кода, определенного в функции main.
В итоге при выполнении программы мы увидим несколько другой консольный вывод:
Теперь строка «Hello Coroutines» не ожидает, пока завершится цикл, а выполняется параллельно с ним.
Вынесение кода корутин в отдельную функцию
Выше код корутины располагался непосредственно в функции main. Но также можно определить его в виде отдельной функции и вызывать в корутине эту функцию:
Корутины и потоки
В ряде языков программирования есть такие структуры, которые позволяют использовать потоки. Однако между корутинами и потоками нет прямого соответствия. Корутина не привязана к конкретному потоку. Она может быть приостановить выполнение в одном потоке, а возобновить выполнение в другом.
Урок 1. Корутины. Введение.
В этом уроке начнем разбираться, что такое корутина и suspend функция.
Сложная тема
Не раз я слышал мнение, что официальная документация по корутинам сложна и представляет собой примерно такое:
Я, пожалуй, соглашусь с этим мнением. Нас сразу грузят билдерами, скоупами и suspend функциями. Говорят, что их надо использовать так-то и так-то и будет нам счастье. И вроде даже объясняют, что это такое, но особо понятнее не становится. Но даже несмотря на это я рекомендую вам посмотреть эту документацию, чтобы получить хотя бы примерное представление, что такое корутины и зачем они нужны.
Поэтому первые несколько уроков будут состоять только из объяснений. Я подробно расскажу о том, во что превращается корутина при преобразовании Kotlin кода в Java. А также о том, почему suspend функция не блокирует поток. Об этом будут первые 5 уроков. А уже после этого пойдут более интересные и приближенные к практике темы: Scope, Context, Job и т.п.
Рекомендую не пропускать уроки и идти по ним последовательно, чтобы в последующих уроках все было понятно. А если какой-то урок или раздел можно будет пропустить, я явно напишу об этом.
Краткий вариант уроков
В этом курсе я буду использовать новую технику подачи материала. Если урок содержит много теории и объяснений, то вы сможете прочитать его в одном из двух вариантов: подробном или кратком.
Подробный вариант подходит для тех, кто в теме новичок. В этом случае требуется максимально подробное объяснение темы. Одна и та же мысль может быть рассмотрена несколько раз с разных сторон для лучшего понимания. Читать надо долго и вдумчиво. В этом стиле написано большинство моих уроков на сложные темы.
Краткий вариант подходит для тех, кто уже хоть немного, но знаком с темой. А также тех, кто ранее уже прочел подробный вариант и зашел просто освежить знания или подсмотреть какой-то момент. Тут не будет подробных объяснений и долгого хождения вокруг темы. Все лаконично и тезисно.
Выбирайте вариант, который подходит вам. Можете сразу попробовать краткий вариант для экономии времени. Если видите, что информации недостаточно, и нужно более вдумчивое объяснение, то заваривайте кружку чая, берите плед и открывайте подробный вариант)
Эта опция может некорректно работать на мобильной версии сайта. Я уже написал в техподдержку создателям, жду ответа. В случае возникновения проблем используйте десктопную версию.
Корутина
Непросто подобрать описание, что такое корутина. В самом начале обычно рассматривается такой простой пример:
Этот и последующие несколько примеров выдернуты из контекста и не будут работать без дополнительного кода. Но в целях изучения теории они нам подходят. Практика будет позже.
Мы еще рассмотрим очень подробно и по шагам, что происходит под капотом билдера. А пока что нам надо знать только то, что билдер упакует переданный ему блок кода в корутину и запустит ее. Если необходимо, то с помощью входных параметров мы можем указать билдеру, в каком потоке необходимо выполнить этот код.
Это дает нам некоторые возможности по управлению корутиной. Мы можем сделать выполнение кода отложенным (LAZY) и стартовать его позже, когда понадобится. Или в любой момент времени можно будет отменить выполнение. Также можно запускать корутину внутри корутины. Их джобы будут связаны между собой отношениями Parent-Child, что является отдельным механизмом, который влияет на обработку ошибок и отмену корутин. Все это мы еще подробно рассмотрим в дальнейших уроках.
Suspend функция
В примере выше мы в корутине используем suspend функцию delay. Эта функция приостановит выполнение кода на 1000 мсек, затем выполнение кода продолжится и напишет в лог слово World. Вроде ничего не обычного. Но обратите внимание, я написал «приостановит выполнение кода», а не «заблокирует поток».
В этом и есть ключевая особенность suspend функции. Она не блочит поток. А значит мы можем эту корутину запускать в main потоке. Функция delay приостановит выполнение кода на 1 секунду, но не заблокирует main поток.
Давайте рассмотрим пример, более приближенный к реальности. Например, загрузка файла.
Код без всяких корутин и suspend функций будет таким:
Мы генерируем URL файла и передаем его в функцию download, которая выполнит загрузку. После этого мы выводим Toast-сообщение о том, что файл загружен.
А хорошо было бы сделать так, чтобы долго работающая функция не блокировала поток, но при этом код, который находится после функции, был выполнен по завершении загрузки без всяких колбэков. Использование suspend функций в корутинах позволяют нам сделать это.
Перепишем код с использованием корутины и suspend функции:
Эта корутина не заблокирует поток, в котором будет запущен ее код. Т.е. его можно запустить даже в main потоке. Функция download загрузит файл в отдельном потоке, а toast будет выполнен только после того, как download отработает.
Это кажется магией, но не забывайте, что Kotlin код будет преобразован в Java классы. И во время этих преобразований будет использован механизм Continuation. Он в сочетании с suspend функциями реализует колбэк, который обеспечит выполнение toast только после того, как загрузка файла будет завершена.
О том, как именно это происходит, подробно поговорим в следующем уроке.
Корутина
В самом начале документации и статей обычно рассматривается такой простой пример:
Когда мы запускаем корутину, мы можем получить Job, как результат запуска билдера:
Это дает нам некоторые возможности по управлению корутиной. Например, мы можем сделать выполнение кода отложенным (lazy) и стартовать его позже, когда понадобится. Или в любой момент времени можно будет отменить выполнение. Также можно запускать корутину внутри корутины. Их джобы будут связаны между собой отношениями Parent-Child, что является отдельным механизмом, который влияет на обработку ошибок и отмену корутин.
Suspend функция
В примере выше мы в корутине используем suspend функцию delay. Эта функция приостановит выполнение кода на 1000 мсек, затем выполнение кода продолжится и напишет в лог слово World. При этом delay не заблокирует поток. В этом и есть ключевая особенность suspend функций. А значит мы можем эту корутину запускать в main потоке.
Давайте рассмотрим пример, более приближенный к реальности. Например, загрузка файла.
Код без всяких корутин и suspend функций будет таким:
Мы генерируем URL файла и передаем его в функцию download, которая выполнит загрузку. После этого мы выводим Toast-сообщение о том, что файл загружен.
Использование suspend функций в корутинах позволяют нам выполнять долгие асинхронные функции без колбэков:
Эта корутина не заблокирует поток, в котором она будет запущена. А toast будет выполнен только после того, как download отработает. Это достигается с помощью механизма Continuation, который используется при преобразовании Kotlin кода в Java классы.
О том, как именно это происходит, подробно поговорим в следующем уроке.
Присоединяйтесь к нам в Telegram:
— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование
— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня
— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме
Комментарии
Спасибо автору за этот курс. Тема выбрана просто идеально, ибо я считаю корутины самой сложной частью котлина (еще конечно обрезки от дженериков, но то таке) и давно хотел с этим разобраться. Рад что заскочил сюда и увидел что есть такой курс.
Урок 3. Корутины. Suspend функции
В этом уроке подробно разберем как создать suspend функции. Также рассмотрим, можно ли блокировать поток, как корутина может потеряться и зачем нужно слово suspend.
В прошлом уроке мы подробно рассмотрели взаимодействие Continuation и suspend функции со стороны Continuation. Теперь посмотрим на это взаимодействие со стороны suspend функций.
Как создать suspend функцию
Сейчас мы будем рассматривать, как создать suspend функцию из асинхронного кода. Если же у вас есть какой-то синхронный метод и его надо сделать suspend, то там просто используется билдер withContext. Об этом мы поговорим, когда начнем тему билдеров.
Давайте создадим suspend функцию download, которую мы использовали ранее в примерах.
Предположим, у нас есть некий NetworkService, который асинхронно умеет загружать файлы. И мы хотим обернуть его в suspend функцию download:
В колбэк onSuccess придет загруженный файл.
Как вы помните из прошлого урока, suspend функция должна результаты своей работы передать в Continuation.invokeSuspend. Для этого используется метод Continuation.resume
Осталось где-то взять continuation. Для этого мы используем функцию suspendCoroutine.
Весь наш код уходит в блок suspendCoroutine. Этот блок дает нам доступ к continuation, в который мы передаем результат работы networkService.download.
Получается, что suspend функция выполнила асинхронную работу и результат передала в continuation.resume, а тот уже передаст его в continuation.invokeSuspend.
Возврат ошибки
Кроме успешного результата, continuation может принять данные об ошибке.
Пусть у NetworkService.Callback есть метод onFailure. Он будет вызван, если при загрузке произошла ошибка. В нем мы вызовем метод continuation.resumeWithException и передадим туда исключение.
В этом случае ошибка не пойдет в метод invokeSuspend, а будет обработана корутиной. Т.е. в этом случае continuation не продолжит свое выполнение и код, который в корутине находится после suspend функции не будет выполнен.
Простая suspend функция download готова. Ее можно вызывать в корутине, она не будет блокировать поток, но приостановит код.
Можно ли сразу вернуть результат
Давайте посмотрим как в Kotlin добавить в suspend функцию такую возможность:
Мы пытаемся получить файл из кэша. Если он там есть, то сразу передаем его в continuation, иначе запускаем асинхронную работу.
Блокирование потока
Когда мы создаем suspend функцию, мы должны позаботиться о том, чтобы она выполнялась асинхронно в другом потоке или вернула результат сразу. Если же мы напишем в suspend функции код, блокирующий поток, то слово suspend нам тут никак не поможет. Такая suspend функция просто заблокирует поток, в котором выполняется корутина.
Давайте рассмотрим пример некорректной suspend функции. Представим, что используемый в примере выше NetworkService теперь работает синхронно. Его метод download не уходит в фоновый поток и не просит колбэк, а загружает файл в текущем потоке и возвращает как результат вызова метода. Т.е. метод networkService.download блокирует поток, в котором он вызван. Если мы используем его напрямую в suspend функции, то получится неправильная suspend функция:
Важно понимать, что ни метод suspendCoroutine, ни слово suspend в описании функции не cделают наш код асинхронным. Метод networkService.download будет выполнен в потоке корутины и заблокирует его. Поэтому от нас требуется обеспечить асинхронность кода в suspend функции, а в качестве колбэка использовать continuation.
О том, как в корутине выполнять синхронный код так, чтобы не блокировать поток корутины, мы поговорим позже, когда будем рассматривать билдеры.
Потерянная корутина
Зачем нужно слово suspend?
Теперь мы знаем достаточно про suspend функции, чтобы поговорить о значении слова suspend. Само по себе это слово не добавляет к функции никакой магии. Оно не сделает так, чтобы синхронный код вдруг перестал блокировать поток. Оно вообще ничего не делает. Это просто маркер того, что данная функция умеет (и должна) работать с Continuation, чтобы приостановить выполнение корутины не блокируя поток.
Давайте взглянем на это с двух точек зрения: создание suspend функции и ее использование.
Создание
Чтобы suspend функция могла приостановить код не блокируя поток, ей нужен Continuation, выполнение которого она возобновит по завершению своей работы. Чтобы получить Continuation, используется функция suspendCoroutine. Возникает вопрос, почему любая другая функция без слова suspend не может этого сделать? Давайте допустим, что мы можем создать обычную (не suspend) функцию и вызвать в ней suspendCoroutine, который предоставит нам Continuation. Если мы вызовем такую функцию в корутине, то все отработает нормально, потому что у корутины есть Continuation. Его мы и получим. Но если мы вызовем ее вне корутины, то suspendCoroutine не сможет предоставить нам Continuation, потому что его просто нет.
Использование
Если мы в корутине собираемся использовать долго работающую функцию, которая помечена как suspend, то мы точно знаем, что эта функция приостановит код и не заблокирует при этом поток, в котором выполняется корутина. Если конечно, она написана корректно)
А обычная функция, которая не является suspend, не сможет такого сделать. Она либо заблокирует поток корутины, либо попросит дать ей колбэк.
Что происходит внутри suspendCoroutine?
Об этом я подробно расскажу в пятом уроке.
Присоединяйтесь к нам в Telegram:
— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование
— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня
— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме
Основы сопрограмм
В этом разделе рассматриваются основные концепции сопрограмм.
Ваша первая сопрограмма
Запустите следующий код:
Результат выполнения кода будет следующим:
Это связано с тем, что delay является функцией приостановки, которая не блокирует поток, а приостанавливает сопрограмму. Использовать её можно только из сопрограммы.
Связываем блокирующий и неблокирующий миры
Таким же образом можно писать модульные тесты для функций приостановки:
Структурированный параллелизм
Есть более лучшее решение. В нашем коде можно использовать структурированный параллелизм. Вместо запуска сопрограмм в GlobalScope, как мы обычно делаем с потоками (потоки всегда глобальные), мы можем запускать сопрограммы в области видимости выполняемой нами операции.
Scope builder
В дополнение к CoroutineScope, предоставляемой разными билдерами, можно объявить свою собственную область видимости с помощью билдера coroutineScope. Он создает область видимости и не завершается, пока не завершатся все запущенные дочерние сопрограммы.
Следующий пример это демонстрирует:
Обратите внимание, что сразу после сообщения «Task from coroutine scope» (во время ожидания выполнения вложенного launch) выполняется и выдаётся «Task from runBlocking», хотя выполнение coroutineScope еще не завершилось.
Извлечение функции
Легковесные сопрограммы
Запустите следующий код:
Данный код запускает 100 тысяч сопрограмм, каждая из которых через 5 секунд печатает точку.
А теперь попробуйте сделать то же самое с потоками. Что произойдёт? (Скорее всего это вызовет ошибку, связанную с нехваткой памяти).
Глобальные сопрограммы похожи на демон-потоки
Нижеприведённый код запускает длительную сопрограмму в GlobalScope, которая два раза в секунду выводит сообщение «I’m sleeping», а затем, после некоторой задержки, происходит возврат из функции main :
Если вы запустите данный код, то увидите, что он трижды выводит сообщение и завершается:
Активные сопрограммы, запущенные в GlobalScope, не поддерживают «жизнь» процесса. В этом они похожи на демон-потоки.
Сопрограммы
Сопрограммы получили статус стабильные в Kotlin 1.3. Детали см. ниже
Некоторые API инициируют долго протекающие операции (такие как сетевой ввод-вывод, файловый ввод-вывод, интенсивная обработка на CPU или GPU и др.), которые требуют блокировки вызывающего кода в ожидании завершения операций. Сопрограммы обеспечивают возможность избежать блокировки исполняющегося потока путём использования более дешёвой и управляемой операции: приостановки (suspend) сопрограммы.
Сопрограммы упрощают асинхронное программирование, оставив все осложнения внутри библиотек. Логика программы может быть выражена последовательно в сопрограммах, а базовая библиотека будет её реализовывать асинхронно для нас. Библиотека может обернуть соответствующие части кода пользователя в обратные вызовы (callbacks), подписывающиеся на соответствующие события, и диспетчировать исполнение на различные потоки (или даже на разные машины!). Код при этом останется столь же простой, как если бы исполнялся строго последовательно.
Многие асинхронные механизмы, доступные в других языках программирования, могут быть реализованы в качестве библиотек с помощью сопрограмм Kotlin. Это включает в себя async / await из C# и ECMAScript, channels и select из языка Go, и generators / yield из C# или Python. См. описания ниже о библиотеках, реализующих такие конструкции.
Блокирование против приостановки
Главным отличительным признаком сопрограмм является то, что они являются вычислениями, которые могут быть приостановлены без блокирования потока (вытеснения средствами операционной системы). Блокирование потоков часто является весьма дорогостоящим, особенно при интенсивных нагрузках: только относительно небольшое число потоков из общего числа является активно выполняющимися, поэтому блокировка одного из них ведет к затягиванию какой-нибудь важной части итоговой работы.
С другой стороны, приостановка сопрограммы обходится практически бесплатно. Не требуется переключения контекста (потоков) или иного вовлечения механизмов операционной системы. И сверх этого, приостановка может гибко контролироваться пользовательской библиотекой во многих аспектах: в качестве авторов библиотеки мы можем решать, что происходит при приостановке, и оптимизировать, журналировать или перехватывать в соответствии со своими потребностями.
Еще одно отличие заключается в том, что сопрограммы не могут быть приостановлены на произвольной инструкции, а только в так называемых точках остановки (приостановки), которые вызываются в специально маркируемых функциях.
Останавливаемые функции
Приостановка происходит в случае вызова функции, обозначенной специальным модификатором suspend :
Такие функции называются функциями остановки (приостановки), поскольку их вызовы могут приостановить выполнение сопрограммы (библиотека может принять решение продолжать работу без приостановки, если результат вызова уже доступен). Функции остановки могут иметь параметры и возвращать значения точно так же, как и все обычные функции, но они могут быть вызваны только из сопрограмм или других функций остановки. В конечном итоге, при старте сопрограммы она должна содержать как минимум одну функцию остановки, и функция эта обычно анонимная (лямбда-функция остановки). Давайте взглянем, для примера, на упрощённую функцию async() (из библиотеки kotlinx.coroutines ):
Продолжая аналогию, await() может быть функцией остановки (также может вызываться из блока async <> ), которая приостанавливает сопрограмму до тех пор, пока некоторые вычисления не будут выполнены, и затем возвращает их результат:
Отметим, что функции приостановки await() и doSomething() не могут быть вызваны из обыкновенных функций, подобных main() :
Заметим, что функции остановки могут быть виртуальными, и при их переопределении модификатор suspend также должен быть указан:
Aннотация @RestrictsSuspension
Внутреннее функционирование сопрограмм
Мы не стремимся здесь дать полное объяснение того, как сопрограммы работают под капотом, но примерный смысл того, что происходит, очень важен.
Сопрограммы полностью реализованы с помощью технологии компиляции (поддержка от языковой виртуальной машины, среды исполнения, или операционной системы не требуется), а приостановка работает через преобразование кода. В принципе, каждая функция приостановки (оптимизации могут применяться, но мы не будем вдаваться в эти подробности здесь) преобразуется в конечный автомат, где состояния соответствуют приостановленным вызовам. Прямо перед приостановкой следующее состояние загружается в поле сгенерированного компилятором класса вместе с сопутствующими локальным переменными и т. д. При возобновлении сопрограммы локальные переменные и состояние восстанавливаются, и конечный автомат продолжает свою работу.
Приостановленную сопрограмму можно сохранять и передавать как объект, который хранит её приостановленное состояние и локальные переменные. Типом таких объектов является Continuation, а преобразование кода, описанное здесь, соответствует классическому Continuation-passing style. Следовательно, приостановливаемые функции принимают дополнительный параметр типа Continuation (сохранённое состояние) под капотом.
Более детально о том, как работают сопрограммы, можно узнать в этом проектном документе. Похожие описания async / await в других языках (таких как C# или ECMAScript 2016) актуальны и здесь, хотя особенности их языковых реализаций могут существенно отличаться от сопрограмм Kotlin.
Экспериментальный статус сопрограмм сменился на стабильный
Важное замечание: мы рекомендовали авторам библиотек, начавшим использовать эксперементальные сопрограммы следовать той же конвенции: добавить к названию суффикс «экспериментальный» (например, com.example.experimental ), указывающий, какой там используется сопрограммно совместимый API. Таким образом ваша библиотека сохранит бинарную совместимость. Сейчас, когда вышел финальный API-интерфейс, выполните следующие действия:
Это позволит минимизировать проблемы миграции для пользователей.
Поддержка экспериментальной версии сопрограмм будет прекращена в Kotlin 1.4
Стандартные API
Сопрограммы представлены в трёх их главных ингредиентах:
Низкий уровень API: kotlin.coroutines
Низкоуровневый API относительно мал и должен использоваться ТОЛЬКО для создания библиотек высокого уровня. Он содержит два главных пакета:
Более детальная информация о использовании этих API может быть найдена здесь.
API генераторов в kotlin.coroutines
Это функции исключительно «уровня приложения» в kotlin.coroutines :
Чтобы продемонстрировать реальную ленивость такой последовательности, давайте напечатаем некоторые отладочные результаты изнутри вызова sequence():
Чтобы сразу породить всю коллекцию (или последовательность) значений, доступна функция yieldAll() :
Функция iterator() во всём подобна sequence(), но только возвращает ленивый итератор.
Другие API высокого уровня: kotlinx.coroutines
Только базовые API, связанные с сопрограммами, доступны непосредственно из стандартной библиотеки Kotlin. Они преимущественно состоят из основных примитивов и интерфейсов, которые, вероятно, будут использоваться во всех библиотеках на основе сопрограмм.
Эти библиотеки являются удобными API, которые делают основные задачи простыми. Также они содержат законченные примеры того, как создавать библиотеки, построенные на сопрограммах.