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