Основы сопроцедур
В этом разделе рассматриваются основные понятия сопроцедур.
Ваша первая сопроцедура
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
Строитель Scope
Помимо scope сопроцедуры, предоставляемой различными строителями, можно объявить свой собственный scope с помощью строителя coroutineScope. Он создаёт scope сопроцедуры и не завершается, пока не завершатся все запущенные дочерние сопроцедуры.
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!
Строитель Scope и параллельность
Строитель coroutineScope может быть использован внутри любой приостанавливаемой функции для выполнения нескольких параллельных операций. Давайте запустим две параллельные сопроцедуры внутри 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 возвращает объект 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 при использовании потоков, может быть выражен с помощью корутин без достижения лимитов ресурсов. Например, следующий код запускает 50 000 отдельных корутин, каждая из которых ждёт 5 секунд, а затем выводит точку ('.') при крайне низком потреблении памяти:
import kotlinx.coroutines.*
fun main() = runBlocking {
repeat(50_000) { // launch a lot of coroutines
launch {
delay(5000L)
print(".")
}
}
}
Если написать ту же программу с помощью потоков (удалить runBlocking, заменить launch на thread, и заменить delay на Thread.sleep), она будет потреблять много памяти. В зависимости от вашей операционной системы, версии JDK и её настроек, либо будет выброшено исключение OutOfMemoryError, либо потоки будут запускаться медленно, так что одновременно будет выполняться не слишком много потоков.
© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/coroutines-basics.html