Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
+277
View File
@@ -0,0 +1,277 @@
---
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)