Корутины
Асинхронное или неблокирующее программирование является важной частью разработки. При создании серверных, десктопных или мобильных приложений важно обеспечить интерфейс не только гибкий с точки зрения пользователя, но и масштабируемый при необходимости.
Kotlin решает эту проблему гибким способом, предоставляя поддержку корутин на уровне языка и делегирование большей части функциональности библиотекам.
В дополнение к возможности асинхронного программирования, корутины также предоставляют множество других возможностей, таких как параллелизм и акторы (действующие субъекты, ориг.: actors).
Как начать
Недавно начали изучать Kotlin? Посетите Начало работы с Kotlin.
Документация
- Руководство по корутинам
- Основы
- Каналы
- Контекст корутин и диспатчеры
- Разделяемое изменяемое состояние и параллелизм
- Асинхронный поток
Руководства
- Методы асинхронного программирования
- Введение в корутины и каналы
- Отладка корутинов с помощью IntelliJ IDEA
- Отладка Kotlin Flow с помощью IntelliJ IDEA – руководство
Корутины
В последнее время поддержка асинхронности и параллельных вычислений стала неотъемлимой чертой многих языков программирования. И Kotlin не является исключением. Зачем нужны асинхронность и параллельные вычисления? Параллельные вычисления позволяют выполнять несколько задач одновременно, а асинхронность позволяет не блокировать основной ход приложения во время выполнения задачи, которая занимает продолжительное время. Например, мы создаем графическое приложение для десктопа или мобильного устройства. И нам надо по нажатию на кнопку отправлять запрос к интернет-ресурсу. Однако подобный запрос может занять продолжительное время. И чтобы приложение не зависало на период отправки запроса, подобные запросы к интернет-ресурсам следует отправлять асинхронно. При асинхронных запросах пользователь не ждет пока придет ответ от интернет-ресурса, а продолжает работу с приложением, а при получении ответа получит соответствующее уведомление.
В языке Kotlin поддержка асинхронности и параллельных вычислений воплощена в виде корутин ( coroutine ). По сути корутина представляет блок кода, который может выполняться параллельно с остальным кодом. А базовая функциональность, связанная с корутинами, сосредоточена в библиотеке kotlinx.coroutines .
Рассмотрим определение и применение корутины на простейшем примере.
Добавление kotlinx.coroutines
Прежде всего стоит отметить, что функциональность корутин (библиотека kotlinx.coroutines ) по умолчанию не включена в проект. И нам ее надо добавить. Если мы создаем проект консольного приложения в IntelliJ IDEA, то мы можем добавить соответствующую библиотеку в проект. Для этого в меню File перейдем к пункту Project Structure..
Далее на вкладке «Project Settings» перейдем к пункту Libraries . В центральном поле отобразятся библиотеки, добавленные в проект.
И для добавления новой библиотеки нажмем на знак плюса и в контекстном меню выберем пункт From Maven.
После этого нам откроется окно для добавления библиотеки через Maven. В этом окне в поле ввода введем название нужной нам библиотеки — kotlinx-coroutines-core-jvm и нажмем на кнопку поиска. Если соответствующая библиотека найдена, то нам отобразится выпадающий список с результатами
Выберем из него последнюю версию, которая называется наподобие org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4 — в данном случае используется версия 1.6.4, но конкретный номер версии может отличаться.
Отметим все необходимые флажки и нажмем на кнопку OK
После установки библиотеки мы сможем найти ее файл в списке библиотек
В качестве альтернативы мы могли бы вручную подключить нужную библиотеку из локального хранилища. Так, на Windows это будет папка C:\Users\[Имя_пользователя]\AppData\Roaming\JetBrains\IdeaIC[номер_версии]\plugins\Kotlin\kotlinc\lib
Далее в этой папке выберем библиотеку kotlinx-coroutines-core-jvm.jar и нажмем на OK для ее добавления:
Определение suspend-функции
Сначала рассмотрим пример, который не использует корутины:
import kotlinx.coroutines.* suspend fun main() < for(i in 0..5)< delay(400L) println(i) >println("Hello Coroutines") >
Здесь в функции main перебираем последовательность от 0 до 5 и выводит текущий элемент последовательности на консоль. Для имитации продолжительной работы внутри цикла вызываем специальную функцию delay() из пакета kotlinx.coroutines . В эту функцию передается количество миллисекунд, на которое выполняется задержка. Передаваемое значение должно иметь тип Long. То есть здесь функция будет выполнять задержку в 400 миллисекунд перед выводом на консоль текущего элемента последовательности.
После выполнения работы цикла выводим на консоль строку «Hello Coroutines».
И чтобы использовать внутри функции main функцию delay() , функция main предваряется модификатром suspend . Модификатор suspend определяет функцию, которая может приостановить свое выполнение и возобновить его через некоторый период времени.
Сама функция delay() тоже является подобной функцией, которая определена с модификатором suspend . А любая функция с модификатором suspend может вызываться либо из другой функции, которая тоже имеет модификатор suspend , либо из корутины.
Если мы запустим приложение, то мы увидим следующий консольный вывод:
0 1 2 3 4 5 Hello Coroutines
Здесь мы видим, что строка «Hello Coroutines» выводится после выполнения цикла. Но вместо цикла у нас могла бы быть более содержательная, но и более продолжительная работа, например, обращение к интернет-ресурсу, к удаленой базе данных, какие-то операции с файлами и т.д. И в этом случае все определенные после этой работы действия ожидали бы завершения этой продолжительной работы, как в данном случае строка «Hello Coroutines» ждет завершения цикла.
Определение корутины
Теперь вынесем продолжительную работу — то есть цикл в корутину:
import kotlinx.coroutines.* suspend fun main() = coroutineScope < launch< for(i in 0..5)< delay(400L) println(i) >> println("Hello Coroutines") >
Прежде всего для определения и выполнения корутины нам надо определить для нее контекст, так как корутина может вызываться только в контексте корутины (coroutine scope). Для этого применяется функция coroutineScope() — создает контекст корутины. Кроме того, эта функция ожидает выполнения всех определенных внутри нее корутин. Стоит отметить, что coroutineScope() может применяться только в функции с модификатором suspend , коей является функция main.
Сама корутина определяется и запускается с помощью построителя корутин — функции launch . Она создает корутину в виде блока кода — в данном случае это:
и запускает эту корутину параллельно с остальным кодом. То есть данная корутина выполняется независимо от прочего кода, определенного в функции main.
В итоге при выполнении программы мы увидим несколько другой консольный вывод:
Hello Coroutines 0 1 2 3 4 5
Теперь строка «Hello Coroutines» не ожидает, пока завершится цикл, а выполняется параллельно с ним.
Вынесение кода корутин в отдельную функцию
Выше код корутины располагался непосредственно в функции main. Но также можно определить его в виде отдельной функции и вызывать в корутине эту функцию:
import kotlinx.coroutines.* suspend fun main()= coroutineScope < launch< doWork() >println("Hello Coroutines") > suspend fun doWork() < for(i in 0..5)< println(i) delay(400L) >>
В данном случае основной код корутины вынесен в функцию doWork() . Поскольку в этой функции применяется функция delay() , то doWork() определена с модификатором suspend . Сама корутина создается также с помощью функции launch() , которая вызывает функцию doWork() .
Обратите внимание, что в примере выше в конце функции main вызывается функция println() , которая выводит строку на консоль. Если мы ее удалим, то мы столкнемся с ошибкой — функция main должна возвращать значение Unit. В этом случае мы можем либо явным образом возвратить значение Unit:
import kotlinx.coroutines.* suspend fun main()= coroutineScope < launch< for(i in 0..5)< println(i) delay(400L) >> Unit >
Либо можно типизировать функцию coroutineScope типом Unit:
import kotlinx.coroutines.* suspend fun main()= coroutineScope < launch< for(i in 0..5)< println(i) delay(400L) >> >
Корутины и потоки
В ряде языков программирования есть такие структуры, которые позволяют использовать потоки. Однако между корутинами и потоками нет прямого соответствия. Корутина не привязана к конкретному потоку. Она может быть приостановить выполнение в одном потоке, а возобновить выполнение в другом.
Когда корутина приостанавливает свое выполнение, например, как в случае выше при вызове задержки с помощью функции delay() , эта корутина освобождает поток, в котором она выполнялась, и сохраняется в памяти. А освобожденный поток может быть зайдествован для других задач. А когда завершается запущенная задача (например, выполнение функции delay() ), корутина возобновляет свою работу в одном из свободных потоков.
Корутины (Сопрограммы). Часть первая
Корутины предназначены для асинхронного программирования и позволяют отказаться от методов обратного вызова (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. Я уже опробовал его и мне он очень понравился.
Цель этой статьи — помочь вам понять Корутины. Просто будьте внимательны при прочтении и у вас всё получится.
Начнем с официального определения Корутин.
Корутины — это новый способ написания асинхронного, неблокирующего кода.
Первый вопрос, возникающий при прочтении этого определения — чем Корутины отличаются от потоков?
Корутины — это облегчённые потоки. Облегчённый поток означает, что он не привязан к нативному потоку, поэтому он не требует переключения контекста на процессор, поэтому он быстрее.
Что это значит, «не привязан к нативному потоку»?
Корутины есть во многих языках программирования.
В принципе, есть два типа Корутин:
- использующие стек;
- неиспользующиие стек;
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
- никто не читает теги