Основы сопрограмм
В этом разделе рассматриваются основные понятия сопрограмм.
Ваша первая сопрограмма
A coroutine — это экземпляр приостанавливаемого вычисления. По концепции она похожа на поток, в том смысле, что она выполняет блок кода, работающий параллельно с остальным кодом. Однако сопрограмма не привязана к какому-либо конкретному потоку. Она может приостановить выполнение в одном потоке и возобновить в другом.
Сопрограммы можно рассматривать как легковесные потоки, но есть ряд важных различий, которые делают их реальное использование сильно отличающимся от использования потоков.
Запустите следующий код, чтобы получить свою первую рабочую сопрограмму:
import kotlinx.coroutines.*
//sampleStart
fun main() = runBlocking { // this: CoroutineScope
launch { // launch a new coroutine and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello") // main coroutine continues while a previous one is delayed
}
//sampleEnd
Вы увидите следующий результат:
Hello World!
Давайте разберем, что делает этот код.
launch — это строитель сопрограмм. Он запускает новую сопрограмму параллельно с остальным кодом, который продолжает работать независимо. Вот почему Hello было напечатано первой.
delay — это специальная приостанавливающая функция. Она приостанавливает сопрограмму на определенное время. Приостановка сопрограммы не блокирует базовый поток, но позволяет другим сопрограммам работать и использовать базовый поток для своего кода.
runBlocking — это также строитель сопрограмм, который связывает мир без сопрограмм обычной fun main() и код с сопрограммами внутри runBlocking { ... } фигурных скобок. Это выделено в IDE подсказкой this: CoroutineScope сразу после runBlocking открывающей фигурной скобки.
Если вы удалите или забудете runBlocking в этом коде, вы получите ошибку в вызове launch, так как launch объявлен только в CoroutineScope:
Unresolved reference: launch
Название runBlocking означает, что поток, который её выполняет (в данном случае — основной поток), блокируется на время вызова, до тех пор, пока все сопрограммы внутри runBlocking { ... } не завершат своё выполнение. Вы часто увидите runBlocking используемым таким образом на самом верхнем уровне приложения и крайне редко внутри реального кода, так как потоки — это дорогостоящие ресурсы, и их блокирование является неэффективным и часто нежелательным.
Структурированная параллельность
Сопрограммы следуют принципу структурированной параллельности, что означает, что новые сопрограммы могут быть запущены только в определенном CoroutineScope, которое ограничивает срок жизни сопрограммы. Приведенный выше пример демонстрирует, что runBlocking устанавливает соответствующий scope, и поэтому предыдущий пример ожидает, пока World! не будет напечатано после задержки в секунду, и только после этого завершается.
В реальном приложении вы будете запускать множество сопрограмм. Структурированная параллельность гарантирует, что они не потеряются и не утекут. Внешний scope не может завершиться, пока не завершатся все его дочерние сопрограммы. Структурированная параллельность также гарантирует, что любые ошибки в коде правильно сообщаются и никогда не теряются.
Рефакторинг: выделение функции
Давайте выделим блок кода внутри launch { ... } в отдельную функцию. При выполнении рефакторинга "Выделить функцию" на этом коде, вы получите новую функцию с модификатором suspend. Это ваша первая приостанавливающая функция. Приостанавливающие функции могут использоваться внутри сопрограмм так же, как и обычные функции, но их дополнительной особенностью является то, что они, в свою очередь, могут использовать другие приостанавливающие функции (например, delay в этом примере), чтобы приостановить выполнение сопрограммы.
import kotlinx.coroutines.*
//sampleStart
fun main() = runBlocking { // this: CoroutineScope
launch { doWorld() }
println("Hello")
}
// this is your first suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}
//sampleEnd
Строитель области
Помимо области сопрограмм, предоставляемой различными билдерами, можно объявить собственную область, используя coroutineScope builder. Он создаёт область сопрограмм и не завершается до тех пор, пока не завершатся все запущенные дочерние сопрограммы.
runBlocking и coroutineScope билдеры могут выглядеть похожими, потому что оба ожидают завершения своего тела и всех его дочерних сопрограмм. Главное отличие в том, что метод runBlocking блокирует текущий поток для ожидания, в то время как coroutineScope просто приостанавливает, освобождая базовый поток для других задач. Из-за этого различия, runBlocking — это обычная функция, а coroutineScope — приостанавливающая функция.
Вы можете использовать coroutineScope из любой приостанавливающей функции. Например, вы можете перенести одновременную печать Hello и World в функцию suspend fun doWorld().
import kotlinx.coroutines.*
//sampleStart
fun main() = runBlocking {
doWorld()
}
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(1000L)
println("World!")
}
println("Hello")
}
//sampleEnd
Этот код также выводит:
Hello World!
Строитель области и параллельность
coroutineScope builder можно использовать внутри любой приостанавливающей функции для выполнения нескольких параллельных операций. Давайте запустим две параллельные сопрограммы внутри doWorld приостанавливающей функции:
import kotlinx.coroutines.*
//sampleStart
// Sequentially executes doWorld followed by "Done"
fun main() = runBlocking {
doWorld()
println("Done")
}
// Concurrently executes both sections
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(2000L)
println("World 2")
}
launch {
delay(1000L)
println("World 1")
}
println("Hello")
}
//sampleEnd
Оба фрагмента кода внутри launch { ... } блоков выполняются параллельно, с World 1 выводимым первым, после секунды с момента запуска, а World 2 выводимым затем, после двух секунд с момента запуска. coroutineScope в doWorld завершается только после завершения обоих, поэтому doWorld возвращается и позволяет вывести строку Done только после этого:
Hello World 1 World 2 Done
Явное задание
launch builder сопрограммы возвращает объект Job, который является обработчиком запущенной сопрограммы и может использоваться для явного ожидания её завершения. Например, вы можете дождаться завершения дочерней сопрограммы, а затем вывести строку "Готово":
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val job = launch { // launch a new coroutine and keep a reference to its Job
delay(1000L)
println("World!")
}
println("Hello")
job.join() // wait until child coroutine completes
println("Done")
//sampleEnd
}
Этот код выводит:
Hello World! Done
Корутины — лёгковесные
Корутины потребляют меньше ресурсов, чем потоки JVM. Код, который исчерпывает доступную память JVM при использовании потоков, может быть выражен с помощью корутин без достижения лимитов ресурсов. Например, следующий код запускает 100000 отдельных корутин, каждая из которых ждёт 5 секунд, а затем выводит точку ('.') при очень малом потреблении памяти:
import kotlinx.coroutines.*
fun main() = runBlocking {
repeat(100_000) { // launch a lot of coroutines
launch {
delay(5000L)
print(".")
}
}
}
Если вы напишете ту же программу, используя потоки (удалите runBlocking, замените launch на thread, и замените delay на Thread.sleep), она, вероятно, потребляет слишком много памяти и выбросит ошибку «недостаточно памяти».
© 2010–2022 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/coroutines-basics.html