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
@@ -0,0 +1,478 @@
---
title: Структурированная конкурентность
description: CoroutineScope, иерархия Job, coroutineScope и supervisorScope, жизненный цикл и GlobalScope
tags:
- kotlin
- coroutines
---
Структурированная конкурентность - главное архитектурное решение корутин: каждая корутина принадлежит области видимости (scope) и имеет родителя. Из этого следуют три гарантии:
1. родитель не завершится, пока не завершатся дети
2. отмена родителя отменяет всех детей
3. ошибка ребёнка доходит до родителя
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
println("начало")
coroutineScope {
launch { delay(100); println("медленный ребёнок") }
launch { delay(20); println("быстрый ребёнок") }
}
println("конец — только после всех детей")
// начало
// быстрый ребёнок
// медленный ребёнок
// конец — только после всех детей
}
```
Без этого свойства асинхронный код неизбежно порождает «потерянные» задачи: никто не знает, сколько их работает и когда они закончатся.
## CoroutineScope
Scope - это объект с одним свойством `coroutineContext`, в котором есть `Job`. Билдеры (`launch`, `async`) - расширения на `CoroutineScope`, поэтому без scope их просто не вызвать.
```kotlin
import kotlinx.coroutines.*
class DataLoader : CoroutineScope {
private val job = SupervisorJob()
override val coroutineContext = job + Dispatchers.Default
fun load(id: Int) = launch {
delay(20L * id) // разные задержки — порядок предсказуем
println("загружено $id")
}
fun close() {
job.cancel() // отменяет всё, что запущено этим scope
}
}
fun main() = runBlocking {
val loader = DataLoader()
loader.load(1)
loader.load(2)
delay(80)
loader.close()
println("загрузчик закрыт")
// загружено 1
// загружено 2
// загрузчик закрыт
}
```
Такой класс владеет своими корутинами: `close()` гарантированно останавливает всё. Это основной способ связать корутины с жизненным циклом компонента.
Чаще используют не наследование, а поле - так интерфейс класса не загрязняется методами scope:
```kotlin
import kotlinx.coroutines.*
class Service2 {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
fun start() {
scope.launch {
delay(20)
println("работа выполнена")
}
}
fun stop() = scope.cancel()
}
fun main() = runBlocking {
val s = Service2()
s.start()
delay(50)
s.stop()
println("остановлено")
// работа выполнена
// остановлено
}
```
Функция `CoroutineScope(context)` - фабрика, а не конструктор интерфейса. Обратите внимание: в `SupervisorJob()` - именно скобки, иначе получится ссылка на конструктор.
## coroutineScope: вложенная область
`coroutineScope { }` - `suspend`-функция, которая создаёт дочернюю область и ждёт её завершения:
```kotlin
import kotlinx.coroutines.*
suspend fun loadAll(): List<String> = coroutineScope {
val a = async { delay(50); "первый" }
val b = async { delay(30); "второй" }
listOf(a.await(), b.await())
}
fun main() = runBlocking {
println(loadAll()) // [первый, второй]
}
```
Это стандартный способ сделать `suspend`-функцию, которая внутри работает параллельно: снаружи она выглядит как обычная функция, возвращающая значение, и гарантированно не оставляет фоновых задач после возврата.
Ошибка внутри отменяет остальных детей:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
try {
coroutineScope {
launch {
try {
delay(1000)
println("не напечатается")
} finally {
println("ребёнка 1 отменили")
}
}
launch {
delay(20)
throw IllegalStateException("ошибка во втором")
}
}
} catch (e: IllegalStateException) {
println("поймано: ${e.message}")
}
// ребёнка 1 отменили
// поймано: ошибка во втором
}
```
Это свойство «всё или ничего»: если часть параллельной работы провалилась, продолжать остальную обычно бессмысленно.
## supervisorScope: независимые дети
Когда дети не должны зависеть друг от друга, нужен `supervisorScope`:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
supervisorScope {
launch {
delay(20)
throw IllegalStateException("первый упал")
}
launch {
delay(50)
println("второй выжил")
}
}
println("область завершилась")
// Exception in thread ... IllegalStateException: первый упал
// второй выжил
// область завершилась
}
```
Ошибка первого не затронула второго. Обратите внимание: исключение всё равно печатается - его нужно обработать, иначе оно уйдёт в обработчик по умолчанию. Подробнее: [Исключения в корутинах](Исключения%20в%20корутинах.md)
| | `coroutineScope` | `supervisorScope` |
|---|---|---|
| ошибка ребёнка | отменяет остальных детей и область | не затрагивает остальных |
| ошибка пробрасывается наверх | да, из блока | нет, идёт в `CoroutineExceptionHandler` |
| отмена сверху | отменяет всех детей | отменяет всех детей |
| когда применять | части одной задачи | независимые подзадачи |
## Иерархия Job
`Job` - дескриптор корутины: её состояние и связи с родителем и детьми.
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val parent = launch {
val child1 = launch { delay(1000) }
val child2 = launch { delay(1000) }
println("детей у родителя: ${coroutineContext[Job]!!.children.count()}")
child1.cancel()
println("child1 отменён: ${child1.isCancelled}")
println("child2 активен: ${child2.isActive}")
child2.cancel()
}
parent.join()
println("родитель завершён: ${parent.isCompleted}")
// детей у родителя: 2
// child1 отменён: true
// child2 активен: true
// родитель завершён: true
}
```
Состояния `Job` и переходы между ними:
```mermaid
stateDiagram-v2
[*] --> New: "launch(start = LAZY)"
[*] --> Active: "launch / async"
New --> Active: "start() / join()"
Active --> Completing: "тело завершилось"
Completing --> Completed: "все дети завершились"
Active --> Cancelling: "cancel() / ошибка"
Completing --> Cancelling: "cancel() / ошибка ребёнка"
Cancelling --> Cancelled: "все дети отменены"
Completed --> [*]
Cancelled --> [*]
```
Флаги состояния:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch { delay(1000) }
println("${job.isActive} ${job.isCompleted} ${job.isCancelled}") // true false false
job.cancel()
job.join()
println("${job.isActive} ${job.isCompleted} ${job.isCancelled}") // false true true
}
```
Важная тонкость: у отменённой корутины `isCompleted` тоже `true` - завершение включает и успешное, и отменённое. Различать их нужно через `isCancelled`.
## Job как часть контекста
`Job` можно передать явно, и тогда корутина станет ребёнком именно его:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val group = Job()
launch(group) { delay(1000); println("не напечатается 1") }
launch(group) { delay(1000); println("не напечатается 2") }
val independent = launch { delay(30); println("независимая выполнилась") }
delay(10)
group.cancel() // отменяет только своих детей
independent.join()
println("готово")
// независимая выполнилась
// готово
}
```
Так делают «группы» задач, отменяемые вместе. Но осторожно: при явном `Job` корутина перестаёт быть ребёнком `runBlocking`, и тот не будет её ждать.
## GlobalScope: почему его не надо
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch {
delay(1000)
println("этого вы не увидите")
}
delay(20)
println("main завершился, не дождавшись")
// main завершился, не дождавшись
}
```
Корутина в `GlobalScope` живёт до конца процесса, её никто не отменит и никто не узнает об ошибке. Аннотация `@DelicateCoroutinesApi` поставлена специально - API помечено как опасное.
Правильные альтернативы:
| Вместо `GlobalScope` | Используйте |
|---|---|
| фоновая задача в сервисе | свой `CoroutineScope` с явным `cancel()` |
| задача на время запроса | `coroutineScope` внутри обработчика |
| задача в Android ViewModel | `viewModelScope` |
| задача в Android UI | `lifecycleScope` |
| задача в тесте | `runTest` |
| утилита или `main` | `runBlocking` |
Единственный оправданный случай - задача, которая действительно должна жить весь процесс (например, периодическая отправка метрик), и даже тогда лучше завести явный `applicationScope`.
## Утечка через внешний launch
```kotlin
import kotlinx.coroutines.*
// плохо: функция возвращается, а работа продолжается неизвестно где
fun CoroutineScope.badLoad(): Unit {
launch { delay(100); println("ещё работаю") }
}
// хорошо: функция suspend и не оставляет хвостов
suspend fun goodLoad(): String = coroutineScope {
val d = async { delay(20); "результат" }
d.await()
}
fun main() = runBlocking {
println(goodLoad()) // результат
badLoad()
println("badLoad вернулся, но задача ещё жива")
delay(150)
// результат
// badLoad вернулся, но задача ещё жива
// ещё работаю
}
```
Правило: публичная функция, выполняющая асинхронную работу, должна быть `suspend` и возвращать результат. Расширение на `CoroutineScope`, запускающее `launch`, оправдано только там, где «запустить и забыть» - это и есть контракт.
## Ожидание нескольких результатов
```kotlin
import kotlinx.coroutines.*
suspend fun loadPage(n: Int): String { delay(20L * n); return "страница $n" }
fun main() = runBlocking {
val pages = coroutineScope {
(1..3).map { async { loadPage(it) } }.awaitAll()
}
println(pages) // [страница 1, страница 2, страница 3]
val results = coroutineScope {
(1..3).map { async { loadPage(it) } }.map { it.await() }
}
println(results) // [страница 1, страница 2, страница 3]
}
```
`awaitAll` отличается от `map { it.await() }` поведением при ошибке: `awaitAll` падает сразу, как только упал любой из `Deferred`, а последовательные `await` дождутся своей очереди.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `ExecutorService` + `shutdown()` | `CoroutineScope` + `cancel()` |
> | `CountDownLatch`, `invokeAll` | `coroutineScope`, `awaitAll` |
> | `Future.cancel` по одной задаче | отмена по дереву |
> | `StructuredTaskScope` (Java 21+) | `coroutineScope` |
> | ошибка в `Runnable` теряется | ошибка идёт родителю |
> | задачи не знают о родителе | иерархия `Job` |
>
> **В Java задачи плоские.** `ExecutorService` - это пул: вы отправляете задачу и получаете `Future`. Если задача запустит ещё три, пул о них ничего не узнает, `shutdown()` их не дождётся, а `Future.cancel` на родителе их не остановит. Отслеживать такие связи приходится вручную - списками `Future`, `CountDownLatch` и флагами.
>
> **`StructuredTaskScope` в Java 21 - прямой аналог `coroutineScope`.** Он появился в том же Project Loom именно потому, что проблема была признана фундаментальной:
> ```java
> try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
> var user = scope.fork(() -> loadUser(id));
> var orders = scope.fork(() -> loadOrders(id));
> scope.join().throwIfFailed();
> return new Result(user.get(), orders.get());
> }
> ```
> Сравните с Kotlin:
> ```kotlin
> import kotlinx.coroutines.*
>
> suspend fun loadUser3(id: Int): String { delay(10); return "u$id" }
> suspend fun loadOrders3(id: Int): List<String> { delay(10); return listOf("o$id") }
>
> suspend fun result(id: Int): String = coroutineScope {
> val user = async { loadUser3(id) }
> val orders = async { loadOrders3(id) }
> "${user.await()} ${orders.await()}"
> }
>
> fun main() = runBlocking { println(result(1)) } // u1 [o1]
> ```
> Идея одна и та же: блок не завершится, пока не завершатся подзадачи. `ShutdownOnFailure` соответствует `coroutineScope`, `ShutdownOnSuccess` - гонке через `select`. Отличие в том, что в Kotlin это единственный способ работы, а в Java - новый опциональный API поверх существующих плоских пулов.
>
> **Ошибка в `Runnable` исчезает.** В `executor.submit(task)` исключение складывается в `Future`, и если `get()` никто не вызовет, об ошибке не узнает никто. В `executor.execute(task)` оно уйдёт в `UncaughtExceptionHandler` потока. Корутины гарантируют, что ошибка дойдёт до родителя и, если не обработана, до обработчика области.
>
> **Завершение пула против отмены области.** `shutdown()` ждёт текущих задач, `shutdownNow()` прерывает потоки, а прерывание работает только на блокирующих вызовах. `scope.cancel()` отменяет всё дерево кооперативно и предсказуемо. Подробнее: [Отмена и таймауты](Отмена%20и%20таймауты.md)
>
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md)
> [!warning] Подводные камни
> **`GlobalScope` - утечка по определению.** Задача не привязана ни к чему, не отменяется при закрытии компонента и может переживать его. В Android это классическая причина обращения к уничтоженному экрану.
>
> **`CoroutineScope(...)` без `cancel()` - та же утечка:**
> ```kotlin
> import kotlinx.coroutines.*
>
> class Leaky {
> private val scope = CoroutineScope(Dispatchers.Default)
> fun start() { scope.launch { delay(10_000) } }
> // нет метода stop — корутина живёт до конца процесса
> }
>
> fun main() = runBlocking {
> Leaky().start()
> println("объект больше не нужен, а корутина работает")
> delay(20)
> // объект больше не нужен, а корутина работает
> }
> ```
> Если класс создаёт scope, у него обязан быть метод освобождения.
>
> **`Job()` вместо `SupervisorJob()` в собственном scope роняет весь scope при первой ошибке:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> val scope = CoroutineScope(Job())
> scope.launch { throw IllegalStateException("первая задача") }
> delay(20)
> val second = scope.launch { println("не запустится") }
> delay(20)
> println("scope отменён: ${!scope.isActive}, вторая отменена: ${second.isCancelled}")
> // scope отменён: true, вторая отменена: true
> }
> ```
> Для долгоживущего scope почти всегда нужен `SupervisorJob`, иначе одна ошибка выводит из строя весь компонент.
>
> **Передача `Job` в билдер разрывает связь с родителем:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> launch(Job()) { // свой Job — runBlocking НЕ ждёт
> delay(200)
> println("не напечатается")
> }
> println("main вышел")
> // main вышел
> }
> ```
> `launch(Job())` выглядит безобидно, но делает корутину сиротой. Чтобы просто сменить диспетчер, передавайте только диспетчер.
>
> **`supervisorScope` не защищает от ошибки в своём теле:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> try {
> supervisorScope {
> launch { delay(1000) }
> throw IllegalStateException("ошибка в теле, не в ребёнке")
> }
> } catch (e: IllegalStateException) {
> println("поймано: ${e.message}") // поймано: ошибка в теле, не в ребёнке
> }
> }
> ```
> Supervisor меняет поведение только для **детей**; код самого блока ведёт себя обычно и отменяет область.
>
> **`coroutineScope` внутри `launch` не делает дочерние ошибки локальными.** `coroutineScope` пробрасывает исключение наверх; если наверху `launch`, оно уйдёт родителю и может уронить всю область. Локализовать нужно через `try/catch` вокруг `coroutineScope` или через `supervisorScope`.
>
> **Наследование `CoroutineScope` классом публикует `launch` в API:**
> ```kotlin
> import kotlinx.coroutines.*
>
> class Exposed : CoroutineScope {
> override val coroutineContext = SupervisorJob() + Dispatchers.Default
> }
>
> fun main() = runBlocking {
> val e = Exposed()
> e.launch { println("любой может запустить корутину в чужом scope") }
> delay(20)
> e.cancel()
> // любой может запустить корутину в чужом scope
> }
> ```
> Внешний код получает возможность запускать задачи в вашем scope. Поле `private val scope` предпочтительнее наследования.
>
> **`cancel()` на scope делает его непригодным навсегда:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> val scope = CoroutineScope(SupervisorJob())
> scope.cancel()
> val job = scope.launch { println("не выполнится") }
> delay(20)
> println("корутина сразу отменена: ${job.isCancelled}") // корутина сразу отменена: true
> }
> ```
> Отменённый scope не «перезапускается» - нужно создавать новый. В Android это причина ошибок при повторном использовании ViewModel-scope.
>
> **`awaitAll` и `map { await() }` ведут себя по-разному при ошибке.** `awaitAll` падает при первой ошибке любого элемента; последовательные `await()` сначала дождутся предыдущих. Для fail-fast нужен `awaitAll`.
>
> **Возврат `Deferred` из публичной функции ломает структурированность.** Вызывающий может не вызвать `await()`, и ошибка потеряется, либо вызвать его после закрытия scope. Возвращайте готовое значение из `suspend`-функции.
Далее: [Билдеры корутин](Билдеры%20корутин.md)