Files
obsidian/Kotlin/Корутины/Корутины.md
T

278 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<String> {
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["сохранить состояние<br/>в Continuation"]
D --> E["вернуть COROUTINE_SUSPENDED<br/>поток свободен"]
E --> F["внешнее событие:<br/>таймер, 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<T>` | `Deferred<T>` или просто `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<String> { 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)