--- title: Корутины description: Что такое приостанавливаемое вычисление, зачем оно нужно и как устроен раздел tags: - kotlin - coroutines --- Корутина - это вычисление, которое можно **приостановить** и продолжить позже, не блокируя поток. Это главный инструмент Kotlin для асинхронного кода: вместо коллбэков и `CompletableFuture` вы пишете последовательный код, а компилятор превращает его в машину состояний. ```kotlin import kotlinx.coroutines.* suspend fun loadUser(id: Int): String { delay(100) // приостановка, а не блокировка return "пользователь $id" } suspend fun loadOrders(userId: Int): List { delay(100) return listOf("заказ 1", "заказ 2") } fun main() = runBlocking { val user = loadUser(1) val orders = loadOrders(1) println("$user: $orders") // пользователь 1: [заказ 1, заказ 2] } ``` Код читается как последовательный, но на `delay` поток освобождается и может выполнять другие корутины. Это и есть вся идея. ## Почему не потоки Поток ОС стоит около мегабайта стека и переключается через ядро. Корутина - это объект в куче, её «переключение» - обычный вызов метода. ```kotlin import kotlinx.coroutines.* fun main() = runBlocking { val jobs = List(50_000) { launch { delay(1000) } } println("запущено корутин: ${jobs.size}") // запущено корутин: 50000 jobs.forEach { it.join() } println("все завершились") // все завершились } ``` Пятьдесят тысяч потоков повалили бы JVM с `OutOfMemoryError`; пятьдесят тысяч корутин спокойно живут в нескольких потоках. Подробнее: [Корутины против потоков](Корутины%20против%20потоков.md) ## Приостановка вместо блокировки Разница принципиальная. Блокировка занимает поток, ничего не делая: ```kotlin import kotlinx.coroutines.* import kotlin.system.measureTimeMillis fun main() = runBlocking { val blocking = measureTimeMillis { withContext(Dispatchers.Default) { List(4) { launch { Thread.sleep(500) } }.forEach { it.join() } } } val suspending = measureTimeMillis { List(4) { launch { delay(500) } }.forEach { it.join() } } println("блокировка дольше: ${blocking > suspending}") // блокировка дольше: true println("приостановка около 500 мс: ${suspending < 900}") // приостановка около 500 мс: true } ``` `Thread.sleep` держит поток; `delay` возвращает его в пул. Из-за этого четыре `delay(500)` на одном потоке занимают 500 мс, а четыре `Thread.sleep(500)` - столько, сколько позволит число потоков. ## Как это работает под капотом Компилятор превращает `suspend`-функцию в машину состояний с объектом-продолжением (`Continuation`). Каждая точка приостановки - это метка, куда вернётся выполнение. ```mermaid graph LR A["вызов suspend-функции"] --> B["выполнение до delay"] B --> C{"нужна приостановка?"} C -->|"нет, результат готов"| G["вернуть значение"] C -->|"да"| D["сохранить состояние
в Continuation"] D --> E["вернуть COROUTINE_SUSPENDED
поток свободен"] E --> F["внешнее событие:
таймер, I/O, другая корутина"] F --> H["resume: восстановить состояние"] H --> B ``` Практическое следствие: приостановка возможна **только** в `suspend`-функции, потому что только у неё есть скрытый параметр `Continuation`. Подробнее: [Suspend-функции](Suspend-функции.md) ## Параллелизм Последовательный код легко превратить в параллельный: ```kotlin import kotlinx.coroutines.* import kotlin.system.measureTimeMillis suspend fun fetchA(): Int { delay(200); return 1 } suspend fun fetchB(): Int { delay(200); return 2 } fun main() = runBlocking { val sequential = measureTimeMillis { val sum = fetchA() + fetchB() println("последовательно: $sum") // последовательно: 3 } val parallel = measureTimeMillis { val a = async { fetchA() } val b = async { fetchB() } println("параллельно: ${a.await() + b.await()}") // параллельно: 3 } println("ускорение примерно вдвое: ${sequential > parallel + 100}") // ускорение примерно вдвое: true } ``` Разница - в `async`: он запускает корутину и сразу возвращает `Deferred`, а `await` ждёт результат. Подробнее: [Билдеры корутин](Билдеры%20корутин.md) ## Структурированная конкурентность Главное архитектурное решение библиотеки: у каждой корутины есть родитель, и родитель не завершится раньше детей. ```kotlin import kotlinx.coroutines.* fun main() = runBlocking { println("начало") coroutineScope { launch { delay(100); println("ребёнок 1") } launch { delay(50); println("ребёнок 2") } } println("конец — после всех детей") // начало // ребёнок 2 // ребёнок 1 // конец — после всех детей } ``` Из этого бесплатно получаются: ожидание завершения, распространение отмены вниз и ошибок вверх, отсутствие «потерянных» задач. Это то, чего в Java приходится добиваться вручную. Подробнее: [Структурированная конкурентность](Структурированная%20конкурентность.md) ## Карта раздела | Заметка | О чём | |---|---| | [Suspend-функции](Suspend-функции.md) | `suspend`, `Continuation`, машина состояний | | [Структурированная конкурентность](Структурированная%20конкурентность.md) | `CoroutineScope`, иерархия, жизненный цикл | | [Билдеры корутин](Билдеры%20корутин.md) | `launch`, `async`, `runBlocking`, `withContext` | | [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md) | на каком потоке выполняется код | | [Отмена и таймауты](Отмена%20и%20таймауты.md) | кооперативная отмена, `withTimeout`, `NonCancellable` | | [Исключения в корутинах](Исключения%20в%20корутинах.md) | распространение ошибок, `SupervisorJob`, обработчики | | [Flow](Flow.md) | асинхронный поток значений, операторы, backpressure | | [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md) | горячие потоки и состояние | | [Channels](Channels.md) | передача значений между корутинами | | [Разделяемое состояние](Разделяемое%20состояние.md) | `Mutex`, атомарные типы, конфайнмент | | [Корутины против потоков](Корутины%20против%20потоков.md) | цифры, стоимость, когда нужны потоки | ## Подключение Корутины - не часть stdlib, это отдельная библиотека: ```kotlin // build.gradle.kts dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0") } ``` В языке есть только `suspend`, `Continuation` и `suspendCoroutine` - минимальная основа. `launch`, `async`, `Flow`, диспетчеры и каналы живут в `kotlinx.coroutines`. Подробнее: [Gradle и Kotlin DSL](../Инструментарий/Gradle%20и%20Kotlin%20DSL.md) > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | `Thread`, `ExecutorService` | корутины на пуле потоков | > | `CompletableFuture` | `Deferred` или просто `suspend fun` | > | `.thenApply().thenCompose()` | последовательный код | > | `CountDownLatch`, `Future.get()` | `join()`, `await()` | > | виртуальные потоки (Java 21+) | корутины | > | `Flow` из Reactive Streams | `kotlinx.coroutines.Flow` | > | отмена через `Future.cancel` и флаги | отмена по иерархии | > > **Главное отличие - асинхронный код выглядит последовательным.** В Java до виртуальных потоков асинхронность означала коллбэки или цепочки `CompletableFuture`: > ```java > loadUser(id) > .thenCompose(user -> loadOrders(user.getId())) > .thenApply(orders -> format(orders)) > .exceptionally(e -> "ошибка: " + e.getMessage()); > ``` > Каждый шаг - лямбда, переменные приходится протаскивать через замыкания, а `try/catch`, циклы и `if` в такой цепочке выражаются неудобно. В Kotlin то же самое пишется обычным кодом: > ```kotlin > import kotlinx.coroutines.* > > suspend fun loadUser2(id: Int): String { delay(10); return "user$id" } > suspend fun loadOrders2(user: String): List { delay(10); return listOf("$user-заказ") } > > suspend fun report(id: Int): String = try { > val user = loadUser2(id) > val orders = loadOrders2(user) > orders.joinToString() > } catch (e: Exception) { > "ошибка: ${e.message}" > } > > fun main() = runBlocking { println(report(1)) } // user1-заказ > ``` > Работают обычные `try/catch`, `for`, `return` и локальные переменные. > > **Виртуальные потоки (Java 21) решают ту же задачу иначе.** Project Loom делает блокировку дешёвой: виртуальный поток при блокирующем вызове освобождает несущий поток ОС, поэтому старый блокирующий код внезапно масштабируется. Разница в подходе: Loom прячет асинхронность от программиста (`Thread.sleep` просто становится дешёвым), корутины делают её видимой в типах (`suspend` в сигнатуре). Плюс Loom - работает весь существующий код; плюс корутин - компилятор знает, где функция может приостановиться, и из этого вырастают структурированная конкурентность, `Flow` и отмена. Корутины также работают на Android и в Kotlin/Native, где Loom недоступен. > > **Отмена в Java не распространяется.** `Future.cancel(true)` ставит флаг прерывания одному потоку; задачи, которые он породил, продолжат работу. В Kotlin отмена идёт по дереву корутин автоматически. Для Java это пришлось бы строить руками поверх `ExecutorService`. Подробнее: [Отмена и таймауты](Отмена%20и%20таймауты.md) > > **Реактивные библиотеки закрывают часть задач.** RxJava и Project Reactor дают потоки данных и операторы, то есть примерный аналог `Flow`. Но у них свой стиль (всё - цепочка операторов), высокий порог входа и плохие стектрейсы. `Flow` при этом - обычные `suspend`-функции внутри, поэтому в нём можно писать `for` и `try`. > > Сравнить: [Многопоточность в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md) > [!warning] Подводные камни > **Корутины не ускоряют вычисления на CPU.** Они экономят потоки при ожидании (сеть, диск, таймеры). Если код считает, а не ждёт, выгоды от корутин нет - там нужен параллелизм по числу ядер: > ```kotlin > import kotlinx.coroutines.* > > fun main() = runBlocking { > val result = withContext(Dispatchers.Default) { // пул по числу ядер > (1..4).map { async { (1..1_000_000).sumOf { x -> x.toLong() } } }.awaitAll() > } > println(result.size) // 4 > } > ``` > На `Dispatchers.IO` такой код только создаст лишние потоки. Подробнее: [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md) > > **Блокирующий вызов внутри корутины блокирует поток целиком:** > ```kotlin > import kotlinx.coroutines.* > import kotlin.system.measureTimeMillis > > fun main() = runBlocking { > val t = measureTimeMillis { > withContext(Dispatchers.Default.limitedParallelism(1)) { > List(3) { launch { Thread.sleep(100) } }.forEach { it.join() } > } > } > println("последовательно, не 100 мс: ${t >= 300}") // последовательно, не 100 мс: true > } > ``` > `Thread.sleep`, `JDBC`, старый `HttpURLConnection`, `File.readText` - всё это блокирует. Такие вызовы нужно оборачивать в `withContext(Dispatchers.IO)`. > > **`runBlocking` блокирует вызывающий поток** - это мост из обычного кода в корутины, а не способ запускать их в продакшене. В Android и серверных фреймворках у вас уже есть scope; `runBlocking` уместен в `main`, тестах и утилитах. > > **Корутина без scope теряется.** `GlobalScope.launch` не привязан ни к чему: его никто не отменит и никто не узнает об ошибке. Это главный антипаттерн в корутинах. Подробнее: [Структурированная конкурентность](Структурированная%20конкурентность.md) > > **Исключение в `launch` не ловится снаружи:** > ```kotlin > import kotlinx.coroutines.* > > fun main() = runBlocking { > try { > launch { throw IllegalStateException("внутри launch") } > } catch (e: Exception) { > println("не поймается здесь") > } > delay(50) > println("а это выполнится") > } > ``` > Этот код напечатает `а это выполнится`, а затем уронит программу: `launch` передаёт ошибку родителю, а не выбрасывает её в месте вызова. Подробнее: [Исключения в корутинах](Исключения%20в%20корутинах.md) > > **Отмена кооперативная.** Корутина, которая не вызывает `suspend`-функций и не проверяет `isActive`, не отменяется: > ```kotlin > import kotlinx.coroutines.* > > fun main() = runBlocking { > val job = launch(Dispatchers.Default) { > var i = 0 > while (i < 100_000_000) i++ // не проверяет отмену > println("досчитал несмотря на отмену") > } > delay(20) > job.cancelAndJoin() > println("cancel вернулся") > } > ``` > Цикл без точек приостановки игнорирует `cancel`. > > **`suspend` в сигнатуре - это заразно.** Вызвать `suspend`-функцию можно только из другой `suspend`-функции или из билдера. Поэтому `suspend` поднимается вверх по стеку до точки входа, и добавить его в середину существующего синхронного кода не получится без переделки всей цепочки вызовов. > > **Версия библиотеки имеет значение.** API корутин развивается: `limitedParallelism`, `Flow`-операторы, `SupervisorJob` поведение менялись между версиями. Код из статей 2019 года может не компилироваться или вести себя иначе. Далее: [Suspend-функции](Suspend-функции.md)