Корутины
В последнее время поддержка асинхронности и параллельных вычислений стала неотъемлимой чертой многих языков программирования. И 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() ), корутина возобновляет свою работу в одном из свободных потоков.
Составление функций приостановки
В этом разделе рассматриваются различные подходы к композиции функций приостановки.
Последовательно по умолчанию
Предположим, что у нас есть две функции приостановки, определенные в другом месте, которые делают что-то полезное, например, какой-то удаленный вызов службы или вычисление. Мы просто притворяемся, что они полезны, но на самом деле каждый из них просто задерживает на секунду для целей этого примера:
suspend fun doSomethingUsefulOne(): Int < delay(1000L) // притворимся, что мы делаем что-то полезное здесь return 13 >suspend fun doSomethingUsefulTwo(): Int < delay(1000L) // притворимся, что мы тоже делаем что-то полезное здесь return 29 >
Что мы делаем, если нам нужно, чтобы они вызывались последовательно — первый doSomethingUsefulOne , затем doSomethingUsefulTwo , и вычислить сумму их результатов? На практике мы делаем это, если используем результат первой функции, чтобы принять решение о том, нужно ли нам вызывать вторую, или решить, как ее вызывать.
Мы используем обычный последовательный вызов, потому что код в корутине, как и в обычном коде, по умолчанию последовательный. Следующий пример демонстрирует это, измеряя общее время, необходимое для выполнения обеих функций приостановки:
import kotlinx.coroutines.* import kotlin.system.* fun main() = runBlocking < val time = measureTimeMillis < val one = doSomethingUsefulOne() val two = doSomethingUsefulTwo() println("The answer is $") > println("Completed in $time ms") > suspend fun doSomethingUsefulOne(): Int < delay(1000L) // притворимся, что мы делаем что-то полезное здесь return 13 >suspend fun doSomethingUsefulTwo(): Int < delay(1000L) // притворимся, что мы тоже делаем что-то полезное здесь return 29 >
You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-compose-01.kt). —>
Получается что-то вроде этого:
The answer is 42 Completed in 2017 ms
Параллельное использование async
Что, если между вызовами doSomethingUsefulOne и doSomethingUsefulTwo нет зависимостей, и мы хотим получить ответ быстрее, выполняя оба одновременно? Здесь на помощь приходит async.
Концептуально async аналогична launch. Она запускает отдельную корутину, представляющую собой облегченный поток, который работает одновременно со всеми другими корутинами. Разница в том, что launch возвращает Job и не несет никакого результирующего значения, тогда как async возвращает Deferred — облегченное неблокирующее будущее значение, которое представляет собой обещание предоставить результат позже. Вы можете использовать .await() для отложенного значения, чтобы получить его окончательный результат, но Deferred также является Job , поэтому при необходимости вы можете отменить его.
import kotlinx.coroutines.* import kotlin.system.* fun main() = runBlocking < val time = measureTimeMillis < val one = async < doSomethingUsefulOne() >val two = async < doSomethingUsefulTwo() >println("The answer is $") > println("Completed in $time ms") > suspend fun doSomethingUsefulOne(): Int < delay(1000L) // притворимся, что мы делаем что-то полезное здесь return 13 >suspend fun doSomethingUsefulTwo(): Int < delay(1000L) // притворимся, что мы тоже делаем что-то полезное здесь return 29 >
You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-compose-02.kt). —>
Получается что-то вроде этого:
The answer is 42 Completed in 1017 ms
Это в два раза быстрее, потому что две корутины выполняются одновременно. Обратите внимание, что параллелизм с корутинами всегда явный.
Ленивый старт async
При желании async можно сделать ленивой, установив для параметра start значение CoroutineStart.LAZY. В этом режиме он запускает корутину только тогда, когда ее результат требуется для await или если вызывается функция start его Job . Запустите следующий пример:
import kotlinx.coroutines.* import kotlin.system.* fun main() = runBlocking < val time = measureTimeMillis < val one = async(start = CoroutineStart.LAZY) < doSomethingUsefulOne() >val two = async(start = CoroutineStart.LAZY) < doSomethingUsefulTwo() >// some computation one.start() // start the first one two.start() // start the second one println("The answer is $") > println("Completed in $time ms") > suspend fun doSomethingUsefulOne(): Int < delay(1000L) // притворимся, что мы делаем что-то полезное здесь return 13 >suspend fun doSomethingUsefulTwo(): Int < delay(1000L) // притворимся, что мы тоже делаем что-то полезное здесь return 29 >
You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-compose-03.kt). —>
Получается что-то вроде этого:
The answer is 42 Completed in 1017 ms
Итак, здесь две корутины определены, но не выполняются, как в предыдущем примере, а программист получает контроль над тем, когда именно начинать выполнение, вызывая start. Сначала мы запускаем one , затем two , а затем ждем завершения отдельных корутин.
Обратите внимание, что если мы просто вызовем await в println без предварительного вызова start для отдельных корутин, это приведет к последовательному поведению, поскольку await запускает выполнение корутины и ожидает ее завершения, что не является предполагаемым «ленивым» вариантом использования. Вариант использования для async(start = CoroutineStart.LAZY) — это замена стандартной функции lazy в случаях, когда вычисление значения включает приостановку функций.
Функции в асинхронном стиле
Мы можем определить функции в асинхронном стиле, которые вызывают doSomethingUsefulOne и doSomethingUsefulTwo асинхронно, используя построитель async корутины с использованием ссылки на GlobalScope для отказа от структурированного параллелизма. Мы называем такие функции суффиксом «. Async», чтобы подчеркнуть тот факт, что они только запускают асинхронные вычисления, и для получения результата необходимо использовать полученное отложенное значение.
[GlobalScope] is a delicate API that can backfire in non-trivial ways, one of which will be explained > below, so you must explicitly opt-in into using `GlobalScope` with `@OptIn(DelicateCoroutinesApi::class)`. —>
GlobalScope — это деликатный API, который может иметь нетривиальные последствия, одно из которых будет объяснено ниже, поэтому вы должны явным образом согласиться на использование GlobalScope с помощью @OptIn(DelicateCoroutinesApi::class) .
// The result type of somethingUsefulOneAsync is Deferred @OptIn(DelicateCoroutinesApi::class) fun somethingUsefulOneAsync() = GlobalScope.async < doSomethingUsefulOne() >// The result type of somethingUsefulTwoAsync is Deferred @OptIn(DelicateCoroutinesApi::class) fun somethingUsefulTwoAsync() = GlobalScope.async
Обратите внимание, что эти функции xxxAsync не являются функциями приостановки. Их можно использовать откуда угодно. Однако их использование всегда подразумевает асинхронное (здесь это означает параллельное) выполнение их действия с вызывающим кодом.
В следующем примере показано их использование вне корутины:
import kotlinx.coroutines.* import kotlin.system.* // обратите внимание, что в этом примере у нас нет `runBlocking` справа от `main` fun main() < val time = measureTimeMillis < // мы можем инициировать асинхронные действия вне корутины val one = somethingUsefulOneAsync() val two = somethingUsefulTwoAsync() // но ожидание результата должно включать либо приостановку, либо блокировку. // здесь мы используем `runBlocking < . >`, чтобы заблокировать основной поток в ожидании результата runBlocking < println("The answer is $") > > println("Completed in $time ms") > @OptIn(DelicateCoroutinesApi::class) fun somethingUsefulOneAsync() = GlobalScope.async < doSomethingUsefulOne() >@OptIn(DelicateCoroutinesApi::class) fun somethingUsefulTwoAsync() = GlobalScope.async < doSomethingUsefulTwo() >suspend fun doSomethingUsefulOne(): Int < delay(1000L) // притворимся, что мы делаем что-то полезное здесь return 13 >suspend fun doSomethingUsefulTwo(): Int < delay(1000L) // притворимся, что мы тоже делаем что-то полезное здесь return 29 >
You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-compose-04.kt). —>
This programming style with async functions is provided here only for illustration, because it is a popular style > in other programming languages. Using this style with Kotlin coroutines is **strongly discouraged** for the > reasons explained below. —>
Этот стиль программирования с асинхронными функциями приведен здесь только для иллюстрации, так как он популярен в других языках программирования. Использование этого стиля с корутинами Kotlin настоятельно не рекомендуется по причинам, описанным ниже.
Рассмотрим, что произойдет, если между строкой val one = somethingUsefulOneAsync() и выражением one.await() в коде будет какая-то логическая ошибка, и программа выдаст исключение, а операция, которую выполняла программа, прервется. Обычно глобальный обработчик ошибок может перехватывать это исключение, регистрировать и сообщать об ошибке разработчикам, но в противном случае программа может продолжать выполнять другие операции. Однако здесь у нас somethingUsefulOneAsync все еще работает в фоновом режиме, даже несмотря на то, что операция, которая его инициировала, была прервана. Эта проблема не возникает при структурированном параллелизме, как показано в разделе ниже.
Структурированный параллелизм с async
Давайте возьмем пример Параллельное использование async и извлечем функцию, которая одновременно выполняет doSomethingUsefulOne и doSomethingUsefulTwo и возвращает сумму их результатов. Поскольку построитель async корутин определен как расширение CoroutineScope, нам нужно иметь его в области действия, и это то, что предоставляет функция coroutineScope:
suspend fun concurrentSum(): Int = coroutineScope < val one = async < doSomethingUsefulOne() >val two = async < doSomethingUsefulTwo() >one.await() + two.await() >
Таким образом, если внутри кода функции concurrentSum что-то пойдет не так, и она выдаст исключение, все корутины, запущенные в ее области действия, будут отменены.
import kotlinx.coroutines.* import kotlin.system.* fun main() = runBlocking < val time = measureTimeMillis < println("The answer is $") > println("Completed in $time ms") > suspend fun concurrentSum(): Int = coroutineScope < val one = async < doSomethingUsefulOne() >val two = async < doSomethingUsefulTwo() >one.await() + two.await() > suspend fun doSomethingUsefulOne(): Int < delay(1000L) // притворимся, что мы делаем что-то полезное здесь return 13 >suspend fun doSomethingUsefulTwo(): Int < delay(1000L) // притворимся, что мы тоже делаем что-то полезное здесь return 29 >
You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-compose-05.kt). —>
У нас все еще есть параллельное выполнение обеих операций, как видно из вывода вышеприведенной функции main :
The answer is 42 Completed in 1017 ms
Отмена всегда распространяется через иерархию корутин:
import kotlinx.coroutines.* fun main() = runBlocking < try < failedConcurrentSum() >catch(e: ArithmeticException) < println("Computation failed with ArithmeticException") >> suspend fun failedConcurrentSum(): Int = coroutineScope < val one = async < try < delay(Long.MAX_VALUE) // Эмулирует очень долгие вычисления 42 >finally < println("First child was cancelled") >> val two = async < println("Second child throws an exception") throw ArithmeticException() >one.await() + two.await() >
You can get the full code [here](../../kotlinx-coroutines-core/jvm/test/guide/example-compose-06.kt). —>
Обратите внимание, как первый async и ожидающий родитель отменяются при сбое одного из дочерних элементов (а именно two ):
Second child throws an exception First child was cancelled Computation failed with ArithmeticException
© 2015—2024 Open Source Community
Сопрограммы
Некоторые API инициируют долго протекающие операции (такие как сетевой ввод-вывод, файловый ввод-вывод, интенсивная обработка на CPU или GPU и др.), которые требуют блокировки вызывающего кода в ожидании завершения операций. Сопрограммы обеспечивают возможность избежать блокировки исполняющегося потока путём использования более дешёвой и управляемой операции: приостановки (suspend) сопрограммы.
Сопрограммы упрощают асинхронное программирование, оставив все осложнения внутри библиотек. Логика программы может быть выражена последовательно в сопрограммах, а базовая библиотека будет её реализовывать асинхронно для нас. Библиотека может обернуть соответствующие части кода пользователя в обратные вызовы (callbacks), подписывающиеся на соответствующие события, и диспетчировать исполнение на различные потоки (или даже на разные машины!). Код при этом останется столь же простой, как если бы исполнялся строго последовательно.
Многие асинхронные механизмы, доступные в других языках программирования, могут быть реализованы в качестве библиотек с помощью сопрограмм Kotlin. Это включает в себя async / await из C# и ECMAScript, channels и select из языка Go, и generators / yield из C# или Python. См. описания ниже о библиотеках, реализующих такие конструкции.
Блокирование против приостановки
Главным отличительным признаком сопрограмм является то, что они являются вычислениями, которые могут быть приостановлены без блокирования потока (вытеснения средствами операционной системы). Блокирование потоков часто является весьма дорогостоящим, особенно при интенсивных нагрузках: только относительно небольшое число потоков из общего числа является активно выполняющимися, поэтому блокировка одного из них ведет к затягиванию какой-нибудь важной части итоговой работы.
С другой стороны, приостановка сопрограммы обходится практически бесплатно. Не требуется переключения контекста (потоков) или иного вовлечения механизмов операционной системы. И сверх этого, приостановка может гибко контролироваться пользовательской библиотекой во многих аспектах: в качестве авторов библиотеки мы можем решать, что происходит при приостановке, и оптимизировать, журналировать или перехватывать в соответствии со своими потребностями.
Еще одно отличие заключается в том, что сопрограммы не могут быть приостановлены на произвольной инструкции, а только в так называемых точках остановки (приостановки), которые вызываются в специально маркируемых функциях.
Останавливаемые функции
Приостановка происходит в случае вызова функции, обозначенной специальным модификатором suspend :
suspend fun doSomething(foo: Foo): Bar
Такие функции называются функциями остановки (приостановки), поскольку их вызовы могут приостановить выполнение сопрограммы (библиотека может принять решение продолжать работу без приостановки, если результат вызова уже доступен). Функции остановки могут иметь параметры и возвращать значения точно так же, как и все обычные функции, но они могут быть вызваны только из сопрограмм или других функций остановки. В конечном итоге при старте сопрограммы она должна содержать как минимум одну функцию остановки, и функция эта обычно анонимная (лямбда-функция остановки). Давайте взглянем, для примера, на упрощённую функцию async() (из библиотеки kotlinx.coroutines ):
fun async(block: suspend () -> T)
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() , она является анонимной функцией остановки, и мы можем вызывать функцию остановки изнутри её:
async
Продолжая аналогию, await() может быть функцией остановки (также может вызываться из блока async <> ), которая приостанавливает сопрограмму до тех пор, пока некоторые вычисления не будут выполнены, и затем возвращает их результат:
async
Больше информации о том, как действительно работают функции async/await в kotlinx.coroutines , может быть найдено здесь.
Отметим, что функции приостановки await() и doSomething() не могут быть вызваны из обыкновенных функций, подобных main() :
fun main(args: Array) < doSomething() // ERROR: Suspending function called from a non-coroutine context >
Заметим, что функции остановки могут быть виртуальными, и при их переопределении модификатор suspend также должен быть указан:
interface Base < suspend fun foo() >class Derived: Base < override suspend fun foo() < . >>
Aннотация @RestrictsSuspension
Расширяющие функции (и анонимные функции) также могут быть маркированы как suspend , подобно и всем остальным (регулярным) функциям. Это позволяет создавать DSL и другие API, которые пользователь может расширять. В некоторых случаях автору библиотеки необходимо запретить пользователю добавлять новые пути приостановки сопрограммы.
Чтобы осуществить это, можно использовать аннотацию @RestrictsSuspension . Когда целевой класс или интерфейс R аннотируется подобным образом, все расширения приостановки должны делегироваться либо из членов R , либо из других его расширений. Поскольку расширения не могут делегировать друг друга до бесконечности (иначе программа никогда не завершится), гарантируется, что все приостановки пройдут посредством вызова члена R , так что автор библиотеки может полностью их контролировать.
Это актуально в тех редких случаях, когда каждая приостановка обрабатывается специальным образом в библиотеке. Например, при реализации генераторов через buildSequence() функцию, описанную ниже, мы должны быть уверены, что любой приостанавливаемый вызовов в сопрограмме завершается вызовом либо yield() , либо yieldAll() , а не какой-либо другой функции. Именно по этой причине SequenceBuilder аннотирована с @RestrictsSuspension:
@RestrictsSuspension public abstract class SequenceBuilder
Внутреннее функционирование сопрограмм
Мы не стремимся здесь дать полное объяснение того, как сопрограммы работают под капотом, но примерный смысл того, что происходит, очень важен.
Сопрограммы полностью реализованы с помощью технологии компиляции (поддержка от языковой виртуальной машины, среды исполнения, или операционной системы не требуется), а приостановка работает через преобразование кода. В принципе, каждая функция приостановки (оптимизации могут применяться, но мы не будем вдаваться в эти подробности здесь) преобразуется в конечный автомат, где состояния соответствуют приостановленным вызовам. Прямо перед приостановкой следующее состояние загружается в поле сгенерированного компилятором класса вместе с сопутствующими локальным переменными и т. д. При возобновлении сопрограммы локальные переменные и состояние восстанавливаются, и конечный автомат продолжает свою работу.
Приостановленную сопрограмму можно сохранять и передавать как объект, который хранит её приостановленное состояние и локальные переменные. Типом таких объектов является 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 относительно мал и должен использоваться ТОЛЬКО для создания библиотек высокого уровня. Он содержит два главных пакета:
- kotlin.coroutines — главные типы и примитивы, такие как:
- createCoroutine()
- startCoroutine()
- suspendCoroutine()
Более детальная информация о использовании этих API может быть найдена здесь.
API генераторов в kotlin.coroutines
Это функции исключительно «уровня приложения» в kotlin.coroutines :
Они перенесены в рамки kotlin-stdlib , поскольку они относятся к последовательностям. По сути, эти функции (и мы можем ограничиться здесь рассмотрением только sequence() ) реализуют генераторы, т. е. предоставляют лёгкую возможность построить ленивые последовательности:
import kotlin.coroutines.* fun main(args: Array) < //sampleStart val fibonacciSeq = sequence < var a = 0 var b = 1 yield(1) while (true) < yield(a + b) val tmp = a + b a = b b = tmp >> //sampleEnd // Print the first eight Fibonacci numbers println(fibonacciSeq.take(8).toList()) >Это сгенерирует ленивую, потенциально бесконечную последовательность Фибоначчи, используя сопрограмму, которая дает последовательные числа Фибоначчи, вызывая функцию yield (). При итерировании такой последовательности на каждом шаге итератор выполняет следующую часть сопрограммы, которая генерирует следующее число. Таким образом, мы можем взять любой конечный список чисел из этой последовательности, например fibonacciSeq.take(8).toList() , дающий в результате [1, 1, 2, 3, 5, 8, 13, 21] . И сопрограммы достаточно дёшевы, чтобы сделать это практичным.
Чтобы продемонстрировать реальную ленивость такой последовательности, давайте напечатаем некоторые отладочные результаты изнутри вызова sequence():
import kotlin.coroutines.* fun main(args: Array) < //sampleStart val lazySeq = sequence < print("START ") for (i in 1..5) < yield(i) print("STEP ") >print("END") > // Print the first three elements of the sequence lazySeq.take(3).forEach < print("$it ") >//sampleEnd >Запустите приведенный выше код, чтобы убедиться, что если мы будем печатать первые три элемента, цифры чередуются со STEP -ами по ветвям цикла. Это означает, что вычисления действительно ленивые. Для печати 1 мы выполняем только до первого yield(i) и печатаем START по ходу дела. Затем, для печати 2 , нам необходимо переходить к следующему yield(i) , и здесь печатать STEP . То же самое и для 3 . И следующий STEP никогда не будет напечатан (точно так же как и END ), поскольку мы никогда не запрашиваем дополнительных элементов последовательности.
Чтобы сразу породить всю коллекцию (или последовательность) значений, доступна функция yieldAll() :
import kotlin.coroutines.* fun main(args: Array) < //sampleStart val lazySeq = sequence < yield(0) yieldAll(1..10) >lazySeq.forEach < print("$it ") >//sampleEnd >Функция iterator() во всём подобна sequence(), но только возвращает ленивый итератор.
Вы могли бы добавить собственную логику выполнения функции sequence() , написав приостанавливаемое расширение класса SequenceScope (что порождается аннотацией @RestrictsSuspension , как описывалось выше):
import kotlin.coroutines.* //sampleStart suspend fun SequenceScope.yieldIfOdd(x: Int) < if (x % 2 != 0) yield(x) >val lazySeq = sequence < for (i in 1..10) yieldIfOdd(i) >//sampleEnd fun main(args: Array) < lazySeq.forEach < print("$it ") >>Другие API высокого уровня: kotlinx.coroutines
Только базовые API, связанные с сопрограммами, доступны непосредственно из стандартной библиотеки Kotlin. Они преимущественно состоят из основных примитивов и интерфейсов, которые, вероятно, будут использоваться во всех библиотеках на основе сопрограмм.
Большинство API уровня приложений, основанные на сопрограммах, реализованы в отдельной библиотеке kotlinx.coroutines . Эта библиотека содержит в себе:
- Платформенно-зависимое асинхронное программирование с помощью kotlinx-coroutines-core :
- этот модуль включает Go-подобные каналы, которые поддерживают select и другие удачные примитивы
- исчерпывающее руководство по этой библиотеке доступно здесь.
Эти библиотеки являются удобными API, которые делают основные задачи простыми. Также они содержат законченные примеры того, как создавать библиотеки, построенные на сопрограммах.
© 2015—2024 Open Source Community
Урок 2. Корутины. Continuation
В этом уроке разберем, что такое Continuation и как suspend функция приостанавливает код, не блокируя поток.
В прошлом уроке мы выяснили что Continuation помогает suspend функциям приостанавливать код, но не блокировать поток. Давайте разберем, как именно это происходит.
Continuation
В качестве примера используем корутину:
launch < val url = buildUrl() download(url) //suspend function toast("File is downloaded: " + url) >download — это suspend функция, которая загружает файл. Позже мы разберем, как создавать такие функции. Пока же я напомню, что suspend функция download не заблокирует поток, в котором она будет вызвана. Потому что свою работу она будет выполнять в отдельном потоке. Но при этом она приостановит выполнение кода. И функция toast будет вызвана только после того, как download загрузит файл. Сейчас мы подробно рассмотрим, как такое возможно. Как вы знаете, Kotlin код будет при компиляции преобразован в Java классы. И корутина — не исключение. Более того, для нее будет создан отдельный класс. Именно в этом классе и содержится механизм приостановки корутины suspend функцией. Т.е. то, что в Kotlin выглядит как магия корутин и suspend функций, под капотом (в Java) реализовано обычным кодом в обычном классе. Но разобраться в коде этого класса — непросто. Поэтому мы начнем с очень упрощенной версии, из которой я убрал почти всю логику. По мере объяснения, я буду логику возвращать, получая код все более похожий на оригинал. Итак, в результате преобразования кода корутины в Java будет создан Continuation класс. Его максимально упрощенная версия выглядит так:
class GeneratedContinuationClass extends SuspendLambda < String url; @Override void invokeSuspend() < url = buildUrl(); download(url); // suspend function toast("File is downloaded: " + url); >>В метод invokeSuspend переехал весь код, который корутина должна выполнить. Этот метод будет вызван при старте корутины. Напомню, что мы обсуждаем, как магия Kotlin реализована под капотом, т.е. — Java код. Здесь уже нет никакой корутины или suspend функции. Они превратились в метод invokeSuspend и асинхронную функцию download (я продолжу называть ее suspend). Основная задача Continuation — сделать так, чтобы toast был выполнен только после того, как функция download в фоновом потоке загрузит файл. Обычно в Java в таких случаях используется колбэк, но Continuation идет другим путем. Код в методе invokeSuspend делится свитчем на две части. И добавляется переменная label.
int label; void invokeSuspend() < switch (label) < case 0: < url = buildUrl(); download(url); // suspend function return; >case 1: < toast("File is downloaded: " + url); return; >> >Точка разделения кода на две части — это suspend функция. Она и весь код перед ней попадет в первую часть. А весь код после нее — во вторую. Теперь при вызове метода invokeSuspend от переменной label зависит, какая из двух частей будет выполнена. Если label = 0, то выполнится первая часть кода (buildUrl + download). А если label = 1, то вторая (toast). Это дает возможность отделить вызов download от toast. Напомню, что первый раз метод invokeSuspend вызывается при старте корутины. При этом вызове отработает первая часть кода (в case 0) и запустится suspend функция, после чего метод invokeSuspend завершится (return). После того, как suspend функция закончит свою работу, надо сделать второй вызов метода invokeSuspend, чтобы отработала вторая часть кода, т.е. toast. Но перед этим вторым вызовом надо поменять значение label с 0 на 1. Это может сделать первая часть кода перед тем, как вызвать suspend функцию.
int label; void invokeSuspend() < switch (label) < case 0: < url = buildUrl(); label = 1; download(url); // suspend function return; >case 1: < toast("File is downloaded: " + url); return; >> >А второй вызов метода invokeSuspend будет поручен suspend функции, т.к. этот вызов должен быть выполнен по ее завершении. Когда suspend функция проходит преобразование из Kotlin в Java, она получает дополнительный входной параметр с типом Continuation. Это интерфейс. И класс, который мы сейчас разбираем, наследует этот интерфейс. Т.е. в suspend функцию передается текущий Continuation объект — this.
int label; void invokeSuspend() < switch (label) < case 0: < url = buildUrl(); label = 1; download(url, this); // suspend function return; >case 1: < toast("File is downloaded: " + url); return; >> >Когда suspend функция закончит загрузку файла, она возьмет Continuation, который ей передали и вызовет его invokeSuspend метод (подробно о том, как это происходит, мы поговорим в уроке про создание suspend функций). Значение label было установлено в 1 (еще до запуска suspend функции), поэтому switch идет во вторую ветку и метод toast будет выполнен. Получается, что Continuation является колбэком для suspend функции. Если в корутине есть несколько suspend функций, то Continuation будет колбэком для всех них. Чуть дальше мы рассмотрим пример с двумя функциями. Давайте еще раз посмотрим на всю цепочку вызовов, чтобы стало понятнее. Я записал видео-презентацию. Используйте паузу, если вам необходимо время, чтобы обдумать какой-либо слайд подробно. Теперь вы примерно представляете себе схему, используемую корутинами и suspend функциями.
Возврат значения suspend функцией
suspend функция может возвращать какое-либо значение, как результат своей работы. Давайте посмотрим, как эта ситуация обрабатывается в Continuation. Кроме этого я добавлю в пример еще одну suspend функцию, чтобы наглядно показать, как switch разделит код в таком случае. Перепишем ранее рассмотренный пример корутины:
launch < val url = buildUrl() val file = download(url) // suspend function toast("File is downloaded: $url") unzip(file) // suspend function toast("File is unzipped") >Функция download теперь возвращает файл, который мы используем в другой suspend функции — unzip. После выполнения unzip нам надо показать сообщение. Код будет разделен на три части:
1) download и все, что перед ней
2) unzip и все, что между ней и download
3) все, что после unzip Точки разделения — это suspend функции. Упрощенный код Continuation мог бы выглядеть примерно так:File file; void invokeSuspend() < switch (label) < case 0: < url = buildUrl(); label = 1; file = download(url, this); // suspend function return; >case 1: < toast("File is downloaded: " + url); label = 2; unzip(file, this); // suspend function return; >case 2: < toast("File is unzipped"); return; >> >Но тут есть проблема. Этот код не будет работать корректно. Обратите внимание на передачу файла между функциями download и unzip. Мы пишем:
file = download(url, this);
Но download при запуске только начнет загрузку файла в фоновом потоке, но не вернет файл сразу как результат вызова. Поэтому такой путь не сработает. Нужен другой способ получить результат от suspend функции download. Давайте вспомним, что suspend функция вызывает invokeSuspend по завершении своей работы. А если она делает этот вызов, то пусть заодно и передает туда результат своей работы, т.е. File. Для этого у invokeSuspend есть входной параметр типа Object:
void invokeSuspend(Object result)
Осталось этот Object привести к типу File. Continuation знает, что функция download вызовет метод invokeSuspend и передаст туда File, как результат своей работы. Во время этого вызова label уже будет равен 1, а значит будет вызван код в ветке case 1. А значит именно там надо делать приведение типа Object к типу File .
File file; void invokeSuspend(Object result) < switch (label) < case 0: < url = buildUrl(); label = 1; download(url, this); // suspend function return; >case 1: < file = (File) result; toast("File is downloaded: " + url); label = 2; unzip(file, this); // suspend function return; >case 2: < toast("File is unzipped"); return; >> >Приводим result к типу File и используем полученный file в функции unzip. Таким образом мы получили результат работы suspend функции download в invokeSuspend. Давайте немного усложним пример и добавим выходной параметр функции unzip:
suspend fun unzip(path: File): Long
unzip возвращает размер распакованного содержимого. Будем использовать это значение в сообщении, которое показываем после выполнения unzip:
launch < val url = buildUrl() val file = download(url) // suspend function toast("File is downloaded: $url") val size = unzip(file) // suspend function toast("File is unzipped, size = $size") >Смотрим Java код:
File file; Long size; void invokeSuspend(Object result) < switch (label) < case 0: < url = buildUrl(); label = 1; download(url, this); // suspend function return; >case 1: < file = (File) result; toast("File is downloaded: " + url); label = 2; unzip(file, this); // suspend function return; >case 2: < size = (Long) result toast("File is unzipped, size javascript:void(0)">КраткоРассмотрим корутину:
launch < val url = buildUrl() download(url) //suspend function toast("File is downloaded") >download - это suspend функция, которая загружает файл. Она не заблокирует поток, в котором выполняется корутина, т.к. для своей работы использует фоновый поток. Но при этом функция toast будет вызвана только после того, как отработает download. Чтобы понять, что такое Continuation, мы взглянем на Java класс, который получится в результате преобразования Kotlin кода корутины. Но сразу разбираться в конечном результате этого преобразования будет слишком сложно. Поэтому мы начнем с упрощенной версии и далее будем добавлять в нее код, делая его более похожим на реальный. Continuation класс:
class GeneratedContinuationClass extends SuspendLambda < int label; String url; void invokeSuspend() < switch (label) < case 0: < url = buildUrl(); label = 1; download(url, this); // suspend function return; >case 1: < toast("File is downloaded: " + url); return; >> > >Обратите внимание на то, что код корутины переехал в отдельный класс. Далее мы будем оперировать объектом этого класса. Нас интересует метод invokeSuspend, где и располагается код корутины. Этот метод будет вызван при старте корутины. Основная задача Continuation - сделать так, чтобы код, расположенный после вызова функции download, был выполнен только когда метод download завершил работу. Для этого код делится свитчем на две части. И добавляется переменная label. Точка разделения кода на две части - это suspend функция. Она и весь код перед ней идет в первую часть. А весь код после нее и до конца корутины идет во вторую часть. От значения переменной label зависит, какая из двух частей будет выполнена при вызове invokeSuspend. Первый раз метод invokeSuspend вызывается при старте корутины. Он выполнит первую часть кода, поменяет значение label на 1 и вызовет suspend функцию. Второй раз метод invokeSuspend будет вызван уже из suspend функции, когда она завершит свою работу. Для этого suspend функция при преобразовании из Kotlin в Java получает дополнительный входной параметр с типом Continuation. Это интерфейс. И класс, который мы сейчас разбираем, наследует этот интерфейс. Т.е. в suspend функцию мы передаем текущий Continuation объект - this. Когда suspend функция закончит загрузку файла, она возьмет Continuation, который ей передали и вызовет его invokeSuspend метод. label был установлен в 1 (еще до запуска suspend функции), поэтому switch пойдет во вторую ветку и метод toast будет выполнен. Таким образом Continuation является колбэком для suspend функции. Если в корутине есть несколько suspend функций, то Continuation будет колбэком для всех них.
Возврат значения suspend функцией
suspend функция может возвращать какое-либо значение, как результат своей работы. Давайте посмотрим, как эта ситуация обрабатывается в Continuation. Кроме этого я добавлю в пример еще одну suspend функцию, чтобы наглядно показать, как switch разделит код в таком случае. Перепишем ранее рассмотренный пример корутины:
launch < val url = buildUrl() val file = download(url) // suspend function toast("File is downloaded: $url") val size = unzip(file) // suspend function toast("File is unzipped, size = $size") >Функция download теперь возвращает файл, который мы используем в еще одной suspend функции - unzip. А unzip в свою очередь возвращает размер распакованного содержимого. Будем использовать это значение в сообщении, которое показываем после выполнения unzip. При преобразовании в java, код будет разделен на три части:
1) download и все, что перед ней
2) unzip и все, что между ней и download
3) все, что после unzip Точки разделения - это suspend функции. Взглянем на полученный из этой корутины метод invokeSuspend. Он стал немного сложнее, т.к. теперь я не стал убирать из него логику получения значений от suspend функций. File file; Long size; void invokeSuspend(Object result) < switch (label) < case 0: < url = buildUrl(); label = 1; download(url, this); // suspend function return; >case 1: < file = (File) result; toast("File is downloaded: " + url); label = 2; unzip(file, this); // suspend function return; >case 2: < size = (Long) result toast("File is unzipped, size custom">Присоединяйтесь к нам в Telegram: - в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п. - в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance - ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня