Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user