Корутины (Сопрограммы). Часть первая
Корутины предназначены для асинхронного программирования и позволяют отказаться от методов обратного вызова (Callback) и RxJava. Самый простой пример применения — вам нужно загрузить картинку с сервера и показать её в ImageView — это две строчки кода. Проблема в том, что на загрузку требуется время и вторая строчка кода должна выполниться не сразу, а после выполнения первой строчки. Нужен механизм ожидания выполнения задачи, после которой можно продолжать работу.
Код с корутинами будет выглядеть в общем виде следующим образом.
launch(Dispatchers.Main) < val image = withContext(Dispatchers.IO) < getImage() >// получить из контекста IO imageView.setImageBitmap(image) // Возвращаемся в главный поток >
Пока функция getImage() выполняется в выделенном пуле потоков IO, главный поток свободен и может выполнять любую другую задачу. Функция withContext() приостанавливает текущую корутину, запущенную через launch(), пока работает getImage(). Как только getImage() выполнит свою задачу, корутина возобновит работу в главном потоке и вызовет imageView.setImageBitmap(image).
Корутины являются аналогом потоков, но более легковесны и проще. Во многих случаях они предпочтительнее потоков, если не требуется интенсивной работы.
Немного теории
Современные операционные системы умеют работать с несколькими потоками в каждом процессе. Поток является абстрактным понятием, создающим иллюзию раздельной работы многих участков кода. Но нужно уметь работать с потоками, чтобы избежать проблем, связанных с кодом, который может блокировать другой код.
В стандартном приложении у вас есть основной поток, в котором мы взаимодействуем с компонентами (кнопки, картинки). Если основной поток заблокирован, то приложение замирает и раздражает пользователя. Особенно ярко проблема выражается, когда приложение требует данных из интернета — это узкое горлышко для приложения. Вам приходится ждать ответа от сервера и приложение вынуждено простаивать.
Другой вариант блокирующего потока — интенсивные вычисления. Даже мощный процессор можно загрузить настолько сложными данными, что он затормозит приложение.
Сами по себе потоки являются тяжёлым ресурсом для системы. В некоторых случаях от потоков нельзя отказаться, других вариантов просто нет. Но у вас есть выбор, какой именно поток заблокировать, чтобы дать свободу другому потоку. Очевидно, не стоит блокировать основной поток приложения. Кроме того, можно вообще не блокировать поток, а позволить ему работать асинхронно, не мешая другому потоку.
Чтобы показать преимущество корутин перед потоками, разработчики из JetBrains приводили следующий пример — Запустим 10 тысяч корутин. Система без труда справится с этой задачей. С таким же количеством потоков программа закроется от нехватки памяти.
(1..10000).forEach < GlobalScope.launch < val threadName = Thread.currentThread().name println("\$it printed on thread \$") > > Thread.sleep(1000)
В целом, корутины — это блок кода, который выполняется асинхронно без блокировки потока, из которого корутина была запущена. Служит заменой устаревшего AsyncTask или схожих технологий в виду своей эффективности.
Сами по себе корутины не являются чем-то новым, они в той или иной форме уже были в других языках программирования даже в 1960-х годах. Kotlin за кулисами использует многопоточность в корутинах в оптимальном режиме.
К сожалению, порог вхождения в корутины очень высок. При знакомстве на котанов (т.е. на нас) обрушивают просто поток (и тут поток, да что же это такое) информации из непонятных и страшных слов. Без стакана (молока) не разберёшься.
Корутины
Асинхронное или неблокирующее программирование является важной частью разработки. При создании серверных, десктопных или мобильных приложений важно обеспечить интерфейс не только гибкий с точки зрения пользователя, но и масштабируемый при необходимости.
Kotlin решает эту проблему гибким способом, предоставляя поддержку корутин на уровне языка и делегирование большей части функциональности библиотекам.
В дополнение к возможности асинхронного программирования, корутины также предоставляют множество других возможностей, таких как параллелизм и акторы (действующие субъекты, ориг.: actors).
Как начать
Недавно начали изучать Kotlin? Посетите Начало работы с Kotlin.
Документация
- Руководство по корутинам
- Основы
- Каналы
- Контекст корутин и диспатчеры
- Разделяемое изменяемое состояние и параллелизм
- Асинхронный поток
Руководства
- Методы асинхронного программирования
- Введение в корутины и каналы
- Отладка корутинов с помощью IntelliJ IDEA
- Отладка Kotlin Flow с помощью IntelliJ IDEA – руководство
Урок 1. Корутины. Введение.
В этом уроке начнем разбираться, что такое корутина и suspend функция.
Сложная тема
Не раз я слышал мнение, что официальная документация по корутинам сложна и представляет собой примерно такое:
Я, пожалуй, соглашусь с этим мнением. Нас сразу грузят билдерами, скоупами и suspend функциями. Говорят, что их надо использовать так-то и так-то и будет нам счастье. И вроде даже объясняют, что это такое, но особо понятнее не становится. Но даже несмотря на это я рекомендую вам посмотреть эту документацию, чтобы получить хотя бы примерное представление, что такое корутины и зачем они нужны.
В защиту авторов документации я должен сказать, что тема действительно очень сложна для объяснения. Для меня корутины по сложности легко обошли такие непростые темы, как Dagger или Backpressure в RxJava. Я потратил кучу времени, чтобы разобраться, что же такое корутины и как они работают. Читал официальные доки, делал примеры, читал статьи, смотрел видео. И все равно у меня оставалось ощущение, что я не понимаю их до конца сам, а значит не могу объяснить другим. Пришлось прибегнуть к последнему, самому надежному и самому сложному средству — лезть в исходники. Врагу не пожелаю туда соваться, но в итоге я таки пробился через эти дебри и постепенно пазл собрался.
В этом курсе я собираюсь достаточно подробно осветить тему корутин, используя для этого все то, что мне удалось раскопать. Я буду шаг за шагом расписывать отдельные кусочки пазла и периодически собирать эти кусочки в большие куски, объясняя очередную тему. В итоге у вас должна сложиться общая картина. Иногда может показаться, что я слишком ухожу в дебри объяснений, и эта информация не нужна вовсе, и давайте лучше сразу с боевых примеров начнем! Но тут я исхожу из своего опыта. Мне удалось полностью понять примеры только после того, как я раскопал внутренности. И сейчас моя цель — сделать так, чтобы вы, посмотрев на код с корутинами, могли точно сказать, как он себя поведет. А для этого нужно понять корутины изнутри.
Поэтому первые несколько уроков будут состоять только из объяснений. Я подробно расскажу о том, во что превращается корутина при преобразовании Kotlin кода в Java. А также о том, почему suspend функция не блокирует поток. Об этом будут первые 5 уроков. А уже после этого пойдут более интересные и приближенные к практике темы: Scope, Context, Job и т.п.
Рекомендую не пропускать уроки и идти по ним последовательно, чтобы в последующих уроках все было понятно. А если какой-то урок или раздел можно будет пропустить, я явно напишу об этом.
Краткий вариант уроков
В этом курсе я буду использовать новую технику подачи материала. Если урок содержит много теории и объяснений, то вы сможете прочитать его в одном из двух вариантов: подробном или кратком.
Подробный вариант подходит для тех, кто в теме новичок. В этом случае требуется максимально подробное объяснение темы. Одна и та же мысль может быть рассмотрена несколько раз с разных сторон для лучшего понимания. Читать надо долго и вдумчиво. В этом стиле написано большинство моих уроков на сложные темы.
Краткий вариант подходит для тех, кто уже хоть немного, но знаком с темой. А также тех, кто ранее уже прочел подробный вариант и зашел просто освежить знания или подсмотреть какой-то момент. Тут не будет подробных объяснений и долгого хождения вокруг темы. Все лаконично и тезисно.
Выбирайте вариант, который подходит вам. Можете сразу попробовать краткий вариант для экономии времени. Если видите, что информации недостаточно, и нужно более вдумчивое объяснение, то заваривайте кружку чая, берите плед и открывайте подробный вариант)
Эта опция может некорректно работать на мобильной версии сайта. Я уже написал в техподдержку создателям, жду ответа. В случае возникновения проблем используйте десктопную версию.
Корутина
Непросто подобрать описание, что такое корутина. В самом начале обычно рассматривается такой простой пример:
launch
Этот и последующие несколько примеров выдернуты из контекста и не будут работать без дополнительного кода. Но в целях изучения теории они нам подходят. Практика будет позже. Здесь launch — это билдер корутины, которому передается небольшой блок кода:
delay(1000L) println("World!")
Мы еще рассмотрим очень подробно и по шагам, что происходит под капотом билдера. А пока что нам надо знать только то, что билдер упакует переданный ему блок кода в корутину и запустит ее. Если необходимо, то с помощью входных параметров мы можем указать билдеру, в каком потоке необходимо выполнить этот код. Если очень упрощать, то можно сказать, что билдер — это экзекьютор, который оборачивает блок кода в Runnable и запускает этот Runnable на выполнение. Т.е. корутину можно представить так:
executor.submit
Казалось бы, все, тему можно закрывать. Корутина — это просто очередной механизм для выполнения асинхронных операций. Но не все так просто. Корутина имеет ряд особенностей. Например — Job. Когда мы запускаем корутину, мы можем получить Job, как результат запуска билдера:
val job = launch
Это дает нам некоторые возможности по управлению корутиной. Мы можем сделать выполнение кода отложенным (LAZY) и стартовать его позже, когда понадобится. Или в любой момент времени можно будет отменить выполнение. Также можно запускать корутину внутри корутины. Их джобы будут связаны между собой отношениями Parent-Child, что является отдельным механизмом, который влияет на обработку ошибок и отмену корутин. Все это мы еще подробно рассмотрим в дальнейших уроках. А пока что нас интересует одна из самых важных особенностей корутин — suspend функции.
Suspend функция
Чтобы лучше понять, что это такое, поговорим про потоки. Поток выполнения корутины — это вовсе не обязательно фоновый поток. Им вполне может быть и main поток. В Android мы часто будем запускать корутины в main потоке. И, как мы все знаем, этот поток нельзя блокировать. В примере выше мы в корутине используем suspend функцию delay. Эта функция приостановит выполнение кода на 1000 мсек, затем выполнение кода продолжится и напишет в лог слово World. Вроде ничего не обычного. Но обратите внимание, я написал «приостановит выполнение кода», а не «заблокирует поток». В этом и есть ключевая особенность suspend функции. Она не блочит поток. А значит мы можем эту корутину запускать в main потоке. Функция delay приостановит выполнение кода на 1 секунду, но не заблокирует main поток. Давайте рассмотрим пример, более приближенный к реальности. Например, загрузка файла. Код без всяких корутин и suspend функций будет таким:
val url = buildUrl() // long synchronous function download(url) toast("File is downloaded")
Мы генерируем URL файла и передаем его в функцию download, которая выполнит загрузку. После этого мы выводим Toast-сообщение о том, что файл загружен. Функция download — синхронная, а значит заблокирует поток, в котором будет выполнена. Но нам хотелось бы выполнить этот код в UI потоке, потому что функция toast может быть выполнена только в нем. Чтобы разрешить это противоречие, нам придется сделать функцию download асинхронной и toast поместить в колбэк, который будет выполнен по завершении загрузки.
val url = buildUrl() // long asynchronous function download(url)
Т.е. при запуске тяжелого кода в main потоке нам приходится выбирать из двух зол: блокировка потока или колбэк. Если функция download синхронна — то поток будет заблокирован. А если она асинхронна, то нам придется добавлять колбэк, чтобы выполнить последующий код. А хорошо было бы сделать так, чтобы долго работающая функция не блокировала поток, но при этом код, который находится после функции, был выполнен по завершении загрузки без всяких колбэков. Использование suspend функций в корутинах позволяют нам сделать это. Перепишем код с использованием корутины и suspend функции:
launch < val url = buildUrl() download(url) //suspend function toast("File is downloaded") >
Эта корутина не заблокирует поток, в котором будет запущен ее код. Т.е. его можно запустить даже в main потоке. Функция download загрузит файл в отдельном потоке, а toast будет выполнен только после того, как download отработает. Это кажется магией, но не забывайте, что Kotlin код будет преобразован в Java классы. И во время этих преобразований будет использован механизм Continuation. Он в сочетании с suspend функциями реализует колбэк, который обеспечит выполнение toast только после того, как загрузка файла будет завершена. О том, как именно это происходит, подробно поговорим в следующем уроке.
Корутина
В самом начале документации и статей обычно рассматривается такой простой пример:
launch
Здесь launch — это билдер корутины, которому передается блок кода. Билдер упакует переданный ему блок кода в корутину и запустит ее. Когда мы запускаем корутину, мы можем получить Job, как результат запуска билдера:
val job = launch
Это дает нам некоторые возможности по управлению корутиной. Например, мы можем сделать выполнение кода отложенным (lazy) и стартовать его позже, когда понадобится. Или в любой момент времени можно будет отменить выполнение. Также можно запускать корутину внутри корутины. Их джобы будут связаны между собой отношениями Parent-Child, что является отдельным механизмом, который влияет на обработку ошибок и отмену корутин.
Suspend функция
В примере выше мы в корутине используем suspend функцию delay. Эта функция приостановит выполнение кода на 1000 мсек, затем выполнение кода продолжится и напишет в лог слово World. При этом delay не заблокирует поток. В этом и есть ключевая особенность suspend функций. А значит мы можем эту корутину запускать в main потоке. Давайте рассмотрим пример, более приближенный к реальности. Например, загрузка файла. Код без всяких корутин и suspend функций будет таким:
val url = buildUrl() // long synchronous function download(url) toast("Url is downloaded")
Мы генерируем URL файла и передаем его в функцию download, которая выполнит загрузку. После этого мы выводим Toast-сообщение о том, что файл загружен. Функция download — синхронная, а значит заблокирует поток, в котором будет выполнена. Но нам хотелось бы выполнить этот код в UI потоке, потому что функция toast может быть выполнена только в нем. Чтобы разрешить это противоречие, нам придется сделать функцию download асинхронной и toast поместить в колбэк, который будет выполнен по завершении загрузки.
val url = buildUrl() // long asynchronous function download(url)
Использование suspend функций в корутинах позволяют нам выполнять долгие асинхронные функции без колбэков:
launch < val url = buildUrl() download(url) //suspend function toast("Url is downloaded") >
Эта корутина не заблокирует поток, в котором она будет запущена. А toast будет выполнен только после того, как download отработает. Это достигается с помощью механизма Continuation, который используется при преобразовании Kotlin кода в Java классы. О том, как именно это происходит, подробно поговорим в следующем уроке.
Присоединяйтесь к нам в Telegram:
— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance
— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня
Комментарии
# Слайдер — Данияр 17.12.2019 21:52
Можно убрать этот слайдер на Подробнее и Кратко. При малейшем Свайпе вверх, резко обзор возвращает на начала текста. В мобильной версии при чтении с браузера. Хочется посмотреть текст выше, свайпаю, и улетаю вверх.
А так описание супер, спасибо.
# Kotlin — jcyjds — Илья 25.12.2019 12:04
Не понятно почему курс на StartAndroid начинается с корутин. Мне например непонятна тема функций высшего порядка: непонятно как их читать — все эти стрилочки, скобочки, обобщения. Не понятно, что куда входит, и куда выходит, какой тип принимается или возвращается. Официальная документация слабо помогла. Понять тот же fold сложно.
# -_- — Дмитрий 09.01.2020 23:45
Илья, есть еще такая штука, называется «книга». Можно купить ее и даже прочитать, там кроются сакральные истины о всех этих волшебных конструкциях языка Котлин — стрелочках, скобочках, обобщениях и вот это вот все.
Спасибо автору за этот курс. Тема выбрана просто идеально, ибо я считаю корутины самой сложной частью котлина (еще конечно обрезки от дженериков, но то таке) и давно хотел с этим разобраться. Рад что заскочил сюда и увидел что есть такой курс.
ЗЫ Я довольно долго и упорно гуглил чтобы понять как работают потроха корутин, но нигде нет инфы! Курс — вышка, рекомендую. Человек потратил время, разобрался, расшарил — красавчик
# Корутины — Анатолий 04.04.2021 10:34
А мне понятно почему Корутины
на первом месте
я изучаю Котлин и часто гружу приложения
контентом и Активити а очень часто
приложения вылетают
хотя код вырный, ошибок нет
а приложения вылетают часто при переходе на другие Активити
Что такое Корутины в Котлине?

Корутины — это отличный функционал, доступный в языке Kotlin. Я уже опробовал его и мне он очень понравился.
Цель этой статьи — помочь вам понять Корутины. Просто будьте внимательны при прочтении и у вас всё получится.
Начнем с официального определения Корутин.
Корутины — это новый способ написания асинхронного, неблокирующего кода.
Первый вопрос, возникающий при прочтении этого определения — чем Корутины отличаются от потоков?
Корутины — это облегчённые потоки. Облегчённый поток означает, что он не привязан к нативному потоку, поэтому он не требует переключения контекста на процессор, поэтому он быстрее.
Что это значит, «не привязан к нативному потоку»?
Корутины есть во многих языках программирования.
В принципе, есть два типа Корутин:
- использующие стек;
- неиспользующиие стек;
Kotlin реализует Корутины без стека — это значит, что в Корутинах нет собственного стека, поэтому они не привязываются к нативному потоку.
И Корутины, и потоки являются многозадачными. Но разница в том, что потоки управляются ОС, а Корутины пользователями.
Теперь вы можете осознанно прочитать и понять выдержку с официального сайта Kotlin:
Корутины можно представить в виде облегчённого потока. Подобно потокам, корутины могут работать параллельно, ждать друг друга и общаться. Самое большое различие заключается в том, что корутины очень дешевые, почти бесплатные: мы можем создавать их тысячами и платить очень мало с точки зрения производительности. Потоки же обходятся дорого. Тысяча потоков может стать серьезной проблемой даже для современной машины.
Давайте посмотрим, как работать с Корутинами
Итак, как запустить корутину (по аналогии с запуском потока)?
Есть две функции для запуска корутины:
launch<> vs async<> в Корутинах Kotlin
Разница в том, что launch<> ничего не возвращает, а async<> возвращает экземпляр Deferred , в котором имеется функция await() , которая возвращает результат корутины, прямо как Future в Java, где мы делаем future.get() для получения результата.
Давайте посмотрим на использование launch<>
fun main(args: Array) < println("Kotlin Start") launch(CommonPool) < delay(2000) println("Kotlin Inside") >println("Kotlin End") >
// The output will be // Kotlin Start // Kotlin End // Kotlin Inside
Этот код запустит новую корутину в данном пуле потоков. В этом случае мы используем CommonPool, который использует ForkJoinPool.commonPool(). Потоки все ещё существуют в программе, основанной на корутинах, но один поток может запускать много корутин, поэтому нет необходимости в слишком большом количестве потоков.
Попробуем одну вещь:
fun main(args: Array)
Если вы cделаете это прямо в основной функции, то получите сообщение об ошибке:
Функции прерывания можно вызвать только из корутины или другой функции прерывания.
Функция задержки является функцией прерывания, соответственно мы можем ее вызывать только из корутины или другой функции прерывания.
Давайте исправим это:
fun main(args: Array) < runBlocking < delay(2000) >>
Ещё один пример:
suspend fun doWorkFor1Seconds(): String
suspend fun doWorkFor2Seconds(): String
// Serial execution private fun doWorksInSeries() < launch(CommonPool) < val one = doWorkFor1Seconds() val two = doWorkFor2Seconds() println("Kotlin One : " + one) println("Kotlin Two : " + two) >>
// The output is // Kotlin One : doWorkFor1Seconds // Kotlin Two : doWorkFor2Seconds
Теперь посмотрим на использование async<>
// Parallel execution private fun doWorksInParallel() < val one = async(CommonPool) < doWorkFor1Seconds() >val two = async(CommonPool) < doWorkFor2Seconds() >launch(CommonPool) < val combined = one.await() + "_" + two.await() println("Kotlin Combined : " + combined) >> // The output is // Kotlin Combined : doWorkFor1Seconds_doWorkFor2Seconds
Т.к. мы используем async<> , то можем вызвать await() для получения результата.
- kotlin
- котлин
- параллельное программирование
- перевод с английского
- программирование
- разработка
- devcolibri
- никто не читает теги
