Корутины Kotlin: быстрый старт

Мы привыкли учиться от простого к сложному, от аксиом к теоремам, от базовых понятий — к сложным концепциям, от «Hello, world!» — к многомодульным приложениям. Но библиотеке Kotlin Coroutines в этом смысле не повезло. И документация, и немногочисленные (особенно на русском языке) учебные материалы с первых страниц оглушают читателя потоком понятий, которые объясняются друг через друга. Приблизительно так: Job — это часть контекста, а контекст — это контейнер, содержащий Job. Раскрутить этот клубок бывает непросто.

Поэтому я пытаюсь рассказать о корутинах по традиционным схемам обучения, начиная от самых простых понятий и постепенно усложняя рассказ. Мне кажется, что все понятия нужно объяснять на примерах — так теория лучше укладывается в голове.

Первая корутина

По определению Дмитрия Жемерова,»корутина — это кусок кода, выполнение которого можно приостановить в одной из указанных точек, а потом с этой точки продолжить».

Давайте посмотрим на код:

import kotlinx.coroutines.*

fun main() = runBlocking {
    print("Hello, ")
    delay(1000)
    print("world!")
}

runBlocking — это функция, которая запускает корутину. В сигнатуре этой функции обратим пока внимание только на второй аргумент, объявленный как

block: suspend CoroutineScope.() -> T

50108dac8e6d5a2d6afb90aa8ded571c.png

block представляет собой лямбду с приемником, то есть функцию, записанную примерно так: SomeType.() -> ReturnType . Будем называть его лямбда-аргументом функции runBlocking или просто блоком.

Как известно, если последний параметр функции в Kotlin сам является функцией, то ее можно в виде лямбда-выражения вынести за круглые скобки (trailing lambda). Поэтому вместо

runBlocking() { ... }

лямбда-аргумент block можно записать внутри фигурных скобок, следующих за именем функции runBlocking:

runBlocking { // лямбда-аргумент }

В приведенном выше коде лямбда-аргумент — это и есть корутина. Очевидно, она печатает «Hello, world!» с паузой 1000 миллисекунд между первым и вторым словом.

Чтобы запустить эту программу, идем на https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core и уточняем актуальную версию библиотеки. В моем случае это 1.10.1.

a30e4d45182cd3d65bd176adab12ddf9.png

Запускаем IntelliJ IDEA, создаем новый проект на Kotlin, указав Gradle в качестве системы сборки. Когда проект создался, открываем в нем файл build.gradle и добавляем в раздел dependencies строчку

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")

Синхронизируем проект, нажав кнопку Load Gradle Changes.

9a6cd5d1f217fb6efae307e38bcd23d7.png

Открываем файл Main.kt и вставляем приведенный выше код. Запускаем программу, убеждаемся, что она действительно печатает «Hello, world!».

Кажется, все банально, если бы не крохотный значок стрелки, пересеченной зеленой волнистой линией.

Точка приостановки корутины
Точка приостановки корутины

Он показывает, что выполнение программы было приостановлено в этой строке во время выполнения функции delay(). Это и есть та самая точка приостановки выполнения корутины, о которой говорил Дмитрий Жемеров. В течение 1000 мс корутина не выполнялась и поток был свободен для других задач. Через 1000 мс корутина продолжила выполнение с того же места, где остановилась.

Вторая корутина

Уложим какую-нибудь задачу в эти 1000 мс, чтобы мы явно увидели, что поток выполнил ее в свободное от основной корутины время. Для этого будем использовать функцию launch.

import kotlinx.coroutines.*
 
 fun main() = runBlocking { // первая корутина
     launch { // вторая корутина
         val word = "beautiful"
         for (ch in word) {
             print("$ch ")
             delay(100)
         }
     }      print("Hello, ")
     delay(1000)
     print("world!")
 }

// Hello, b e a u t i f u l world!

Вот что здесь выводится на печать:

Время, мс

Печать

Источник

0

Hello,

Корутина #1

~10

b

Корутина #2

110

e

Корутина #2

210

a

Корутина #2

310

u

Корутина #2

410

t

Корутина #2

510

i

Корутина #2

610

f

Корутина #2

710

u

Корутина #2

810

l

Корутина #2

1000

world!

Корутина #1

 А если бы код внутри launch замешкался? Замените внутри нее

delay(100)

на

delay(500)

Теперь мы получим «Hello, b e world! a u t i f u l» и вот почему.

Время, мс

Печать

Источник

0

Hello,

Корутина #1

~10

b

Корутина #2

510

e

Корутина #2

1000

world!

Корутина #1

1010

a

Корутина #2

1510

u

Корутина #2

2010

t

Корутина #2

2510

i

Корутина #2

3010

f

Корутина #2

3510

u

Корутина #2

4010

l

Корутина #2

 Обратите внимание, что надпись «Hello,» появилась в первую очередь, а буква b — только после нее. Следовательно, delay() отработала раньше, чем launch, хотя в коде и находится после нее. Дело в том, что delay() находится в главной корутине и попадает в очередь диспетчера раньше. Вторая корутина, создающаяся при вызове launch, ставится в очередь позже.

Отметим, что первая корутина работает синхронно, а вторая — асинхронно, поэтому она может начать выполнять свой код с небольшой задержкой относительно первой корутины.

886c71d95943c3940b97a7601e81345e.png

Сейчас главное, что нам нужно понять из этого примера, — отсутствие блокировки потока. Пока первая корутина приостановлена с помощью delay(), вторая не теряет времени даром, по мере сил печатая буквы.

suspend

Как видно из названия, функция delay() вызывает задержку. Но не программы в целом, а лишь корутины. В сигнатуре этой функции есть модификатор suspend.

5d8db3de31fecaba26ee41852773a8bc.png

Suspend-функция — это функция, которая может инициировать приостановку (suspend) корутины без блокировки потока и затем ее возобновление (resume) с того же места и в том же состоянии (локальные переменные, стек вызовов). Получив сигнал приостановки от suspend-функции, корутина «засыпает» и освобождает поток до получения сигнала о возобновлении. А функция тем временем выполняет свою работу.

16a89b359c84e500c864371fc1c9ee16.png

Именно способность приостанавливаться и возобновляться делает корутины мощным способом реализации конкурентности, в отличие от потоков, которые могут только блокироваться. Это напоминает кнопку «Пауза» на беговой дорожке: тренировка приостанавливается, но счетчики времени, километров и калорий не сбрасываются. Если мы представим себе продвинутую беговую дорожку, которая во время пауз сохраняет индивидуальный прогресс каждого пользователя, то мы получим приблизительную картину работы корутин.

Как это работает? К сигнатуре suspend-функции компилятор добавляет скрытый параметр типа Continuation, где T — это тип результата, который она должна вернуть. Этот параметр хранит все необходимые данные, чтобы приостановленное вычисление можно было продолжить без потери информации. Так реализуется принцип, известный в функциональном программировании как стиль передачи продолжений (Continuation Passing Style), когда управление явно передается в виде продолжения.

В данном случае объект Continuation действует как колбэк: это функция, которая будет вызвана позже (с помощью метода resume() или resumeWith()), когда результат вычисления станет доступен. Колбэк (callback) — это механизм, позволяющий отложить выполнение части кода до наступления определенного события, в нашем случае — до завершения suspend‑функции.

Код каждой suspend-функции компилятор преобразует в конечный автомат, где каждая точка приостановки соответствует отдельному состоянию автомата. Полученный механизм очень удобно двигать пошагово, то ставя его на паузу, то возобновляя. Номер текущего состояния (label) и значения локальных переменных сохраняются в объекте Continuation, что позволяет шаг за шагом возобновлять выполнение функции с того места, где оно было приостановлено.

Для вложенных suspend-функций будут сгенерированы собственные конечные автоматы, которым будет передано управление в виде объекта Continuation.

Возвращение из этих сложных структур происходит так же по цепочке с помощью методов resume*().

e45ccf872cce4fa958b2e3648bc9b59c.png

Подробнее о работе корутин под капотом можно прочитать в статье Мануэля Виво или в книге Марчина Москала «Kotlin Coroutines. Глубокое погружение» (стр. 39–56).

Важно запомнить, что из suspend-функции можно вызвать обычную функцию, а вот обратное неверно. Suspend-функции могут вызываться либо внутри корутин, либо внутри других suspend-функций.

Точки приостановки. delay и yield

Указание модификатора suspend само по себе еще не превращает функцию в точку приостановки. Чтобы заслужить это гордое имя, функции придется вызвать одну из функций, которые действительно приостанавливают выполнение (например, delay, withContext, yield и т. д.), или реализовать аналогичное поведение самостоятельно с помощью suspendCoroutine или suspendCancellableCoroutine.

suspend fun notSuspending() {
    println("Это suspend-функция, но она не приостанавливается")
}
suspend fun suspending() {
    delay(1000) // Здесь происходит реальная приостановка
}

Как уже говорилось, функция delay() выступает как точка приостановки, где корутина засыпает, уступая управление другому коду, и где она затем просыпается для возобновления работы.

Параметр delay() — число миллисекунд для задержки. Оно может быть и нулевым, и отрицательным. Оба случая просто сведут действие функции на нет. Следующие две строчки сработают одинаково, то есть никак:

delay(-100)
delay(0)

Кроме delay(), существуют и другие точки приостановки. Обсудим для начала интересную функцию yield(). Она позволяет текущей корутине добровольно уступить выполнение другим корутинам. Таким образом обеспечивается справедливое распределение процессорного времени между корутинами.

import kotlinx.coroutines.*

fun main(): Unit = runBlocking {
    // Корутина, имитирующая Чичикова
    launch {
        repeat(5) { step ->
            println("Чичиков, шаг $step: уступает дверь Манилову")
            yield() // уступает управление
        }
    }
    // Корутина, имитирующая Манилова
    launch {
        repeat(5) { step ->
            println("Манилов, шаг $step: уступает дверь Чичикову")
            yield() // уступает управление
        }
    }
}

Запустите программу, чтобы увидеть, как два персонажа безуспешно пытаются попасть в гостиную, пропуская друг друга вперед.

Итоги

Итак, мы разобрались, что такое корутина, и научились их запускать. Кроме того, мы узнали, как работает suspend-функция и где находятся точки приостановки корутины.

Об этом и многом другом вы можете подробнее прочитать в моем курсе по Kotlin Coroutines на Stepik. Промокод с говорящим названием SUSPEND дает 50%-ную скидку на покупку курса.

© Habrahabr.ru