Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,545 @@
|
||||
---
|
||||
title: Channels
|
||||
description: Передача значений между корутинами: типы буферов, закрытие, fan-out и fan-in, select и когда канал лучше Flow
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
- channels
|
||||
---
|
||||
|
||||
`Channel` - очередь для передачи значений между корутинами. Если `Flow` описывает, как значения производятся, то `Channel` - это объект, в который одна корутина кладёт значения, а другая забирает.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val channel = Channel<Int>()
|
||||
|
||||
launch {
|
||||
for (i in 1..3) {
|
||||
channel.send(i) // приостановится, пока никто не принял
|
||||
}
|
||||
channel.close()
|
||||
}
|
||||
|
||||
for (value in channel) { // завершится при close()
|
||||
println("получено: $value")
|
||||
}
|
||||
// получено: 1
|
||||
// получено: 2
|
||||
// получено: 3
|
||||
}
|
||||
```
|
||||
`send` и `receive` - `suspend`-функции: отправитель ждёт получателя, получатель ждёт отправителя. Это rendezvous-канал, вариант по умолчанию.
|
||||
|
||||
## Типы каналов
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
// RENDEZVOUS (по умолчанию): буфера нет
|
||||
val rendezvous = Channel<Int>()
|
||||
println("rendezvous принял без получателя: ${rendezvous.trySend(1).isSuccess}")
|
||||
|
||||
// BUFFERED: буфер на n элементов
|
||||
val buffered = Channel<Int>(capacity = 2)
|
||||
println("в буфер на 2: ${buffered.trySend(1).isSuccess} ${buffered.trySend(2).isSuccess}")
|
||||
println("третий не влез: ${buffered.trySend(3).isSuccess}")
|
||||
|
||||
// CONFLATED: хранит только последнее
|
||||
val conflated = Channel<Int>(Channel.CONFLATED)
|
||||
conflated.trySend(1); conflated.trySend(2); conflated.trySend(3)
|
||||
println("conflated хранит последнее: ${conflated.receive()}")
|
||||
|
||||
// UNLIMITED: растёт без ограничений
|
||||
val unlimited = Channel<Int>(Channel.UNLIMITED)
|
||||
repeat(1000) { unlimited.trySend(it) }
|
||||
println("unlimited принял всё: ${unlimited.receive()}")
|
||||
// rendezvous принял без получателя: false
|
||||
// в буфер на 2: true true
|
||||
// третий не влез: false
|
||||
// conflated хранит последнее: 3
|
||||
// unlimited принял всё: 0
|
||||
}
|
||||
```
|
||||
| Тип | Поведение `send` при заполнении |
|
||||
|---|---|
|
||||
| `RENDEZVOUS` (0) | ждёт, пока кто-то вызовет `receive` |
|
||||
| `BUFFERED` (по умолчанию 64) | ждёт освобождения места |
|
||||
| `CONFLATED` | не ждёт, заменяет непрочитанное значение |
|
||||
| `UNLIMITED` | не ждёт никогда, растёт в памяти |
|
||||
| `capacity = n` + `onBufferOverflow` | `DROP_OLDEST` / `DROP_LATEST` вместо ожидания |
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val dropping = Channel<Int>(capacity = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
repeat(5) { dropping.trySend(it) }
|
||||
println("осталось последние два: ${dropping.receive()} ${dropping.receive()}")
|
||||
// осталось последние два: 3 4
|
||||
}
|
||||
```
|
||||
|
||||
## Закрытие канала
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val channel = Channel<Int>(capacity = 3)
|
||||
channel.send(1)
|
||||
channel.send(2)
|
||||
channel.close()
|
||||
|
||||
println("после close ещё можно читать: ${channel.receive()} ${channel.receive()}")
|
||||
println("отправка запрещена: ${channel.trySend(3).isSuccess}")
|
||||
println("закрыт для отправки: ${channel.isClosedForSend}")
|
||||
|
||||
val error = runCatching { channel.receive() }.exceptionOrNull()
|
||||
println("чтение из пустого закрытого: ${error is ClosedReceiveChannelException}")
|
||||
// после close ещё можно читать: 1 2
|
||||
// отправка запрещена: false
|
||||
// закрыт для отправки: true
|
||||
// чтение из пустого закрытого: true
|
||||
}
|
||||
```
|
||||
`close()` - это «больше значений не будет»: уже отправленные элементы дочитываются, новые не принимаются. `for`-цикл по каналу завершается нормально, а `receive()` на исчерпанном канале бросает исключение. Безопасная альтернатива - `receiveCatching`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val channel = Channel<Int>()
|
||||
channel.close()
|
||||
|
||||
val result = channel.receiveCatching()
|
||||
println("успех: ${result.isSuccess}, закрыт: ${result.isClosed}")
|
||||
println("значение или null: ${result.getOrNull()}")
|
||||
// успех: false, закрыт: true
|
||||
// значение или null: null
|
||||
}
|
||||
```
|
||||
|
||||
Закрытие с ошибкой передаёт её получателю:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val channel = Channel<Int>()
|
||||
launch {
|
||||
channel.send(1)
|
||||
channel.close(IllegalStateException("источник сломался"))
|
||||
}
|
||||
|
||||
val error = runCatching {
|
||||
for (v in channel) println("получено: $v")
|
||||
}.exceptionOrNull()
|
||||
println("ошибка дошла: ${error?.message}")
|
||||
// получено: 1
|
||||
// ошибка дошла: источник сломался
|
||||
}
|
||||
```
|
||||
|
||||
## produce: канал как результат корутины
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun CoroutineScope.produceSquares(n: Int): ReceiveChannel<Int> = produce {
|
||||
for (i in 1..n) {
|
||||
delay(10)
|
||||
send(i * i)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val squares = produceSquares(4)
|
||||
println(squares.toList()) // [1, 4, 9, 16]
|
||||
}
|
||||
```
|
||||
`produce` закрывает канал автоматически при завершении тела - это его главное преимущество перед ручным `Channel()` + `launch`. Он помечен `@ObsoleteCoroutinesApi`, но остаётся самым аккуратным способом создать канал-источник.
|
||||
|
||||
## Fan-out: несколько получателей
|
||||
Канал распределяет элементы между получателями - каждый элемент достаётся **ровно одному**:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
fun main() = runBlocking {
|
||||
val tasks = Channel<Int>()
|
||||
val handled = ConcurrentHashMap<Int, Int>()
|
||||
|
||||
repeat(3) { worker ->
|
||||
launch {
|
||||
for (task in tasks) {
|
||||
delay(10)
|
||||
handled[task] = worker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repeat(9) { tasks.send(it) }
|
||||
tasks.close()
|
||||
delay(100)
|
||||
|
||||
println("обработано задач: ${handled.size}")
|
||||
println("каждая ровно раз: ${handled.keys.sorted() == (0..8).toList()}")
|
||||
println("работали несколько воркеров: ${handled.values.toSet().size > 1}")
|
||||
// обработано задач: 9
|
||||
// каждая ровно раз: true
|
||||
// работали несколько воркеров: true
|
||||
}
|
||||
```
|
||||
Это главное отличие канала от `SharedFlow`: канал **распределяет**, `SharedFlow` **транслирует** всем. Пул воркеров без канала не сделать.
|
||||
|
||||
## Fan-in: несколько отправителей
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val results = Channel<String>()
|
||||
|
||||
launch { delay(10); results.send("от первого") }
|
||||
launch { delay(20); results.send("от второго") }
|
||||
launch { delay(30); results.send("от третьего") }
|
||||
|
||||
repeat(3) { println(results.receive()) }
|
||||
results.close()
|
||||
// от первого
|
||||
// от второго
|
||||
// от третьего
|
||||
}
|
||||
```
|
||||
|
||||
## Конвейер
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun CoroutineScope.numbers(): ReceiveChannel<Int> = produce {
|
||||
var i = 1
|
||||
while (i <= 5) send(i++)
|
||||
}
|
||||
|
||||
fun CoroutineScope.squares(input: ReceiveChannel<Int>): ReceiveChannel<Int> = produce {
|
||||
for (n in input) send(n * n)
|
||||
}
|
||||
|
||||
fun CoroutineScope.evens(input: ReceiveChannel<Int>): ReceiveChannel<Int> = produce {
|
||||
for (n in input) if (n % 2 == 0) send(n)
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val pipeline = evens(squares(numbers()))
|
||||
println(pipeline.toList()) // [4, 16]
|
||||
coroutineContext.cancelChildren()
|
||||
}
|
||||
```
|
||||
Каждая стадия - отдельная корутина, работающая параллельно с остальными. Для такой задачи обычно достаточно `Flow` с операторами, но каналы дают контроль над буферами каждой стадии.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["produce: 1..5"] -->|"Channel"| B["squares"]
|
||||
B -->|"Channel"| C["evens"]
|
||||
C -->|"Channel"| D["collect"]
|
||||
E["Каждая стадия —<br/>своя корутина"] -.-> B
|
||||
```
|
||||
|
||||
## Тикер и таймеры
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val ticks = mutableListOf<Int>()
|
||||
val job = launch {
|
||||
var i = 0
|
||||
while (true) {
|
||||
delay(20)
|
||||
ticks += i++
|
||||
}
|
||||
}
|
||||
delay(70)
|
||||
job.cancel()
|
||||
println("тиков примерно 3: ${ticks.size in 2..4}") // тиков примерно 3: true
|
||||
}
|
||||
```
|
||||
Для периодических задач проще цикл с `delay`, чем `ticker` (который помечен устаревшим). Если нужен именно `Flow`, его даёт `flow { while (true) { delay(x); emit(Unit) } }`.
|
||||
|
||||
## select: ждать первое из нескольких
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
import kotlinx.coroutines.selects.select
|
||||
|
||||
fun main() = runBlocking {
|
||||
val fast = Channel<String>()
|
||||
val slow = Channel<String>()
|
||||
|
||||
launch { delay(10); fast.send("быстрый") }
|
||||
launch { delay(50); slow.send("медленный") }
|
||||
|
||||
val first = select<String> {
|
||||
fast.onReceive { "пришло из fast: $it" }
|
||||
slow.onReceive { "пришло из slow: $it" }
|
||||
}
|
||||
println(first)
|
||||
coroutineContext.cancelChildren()
|
||||
// пришло из fast: быстрый
|
||||
}
|
||||
```
|
||||
`select` работает и с `Deferred` - это способ устроить гонку запросов:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.selects.select
|
||||
|
||||
fun main() = runBlocking {
|
||||
val mirror1 = async { delay(50); "зеркало 1" }
|
||||
val mirror2 = async { delay(10); "зеркало 2" }
|
||||
|
||||
val winner = select<String> {
|
||||
mirror1.onAwait { it }
|
||||
mirror2.onAwait { it }
|
||||
}
|
||||
println("ответил первым: $winner")
|
||||
coroutineContext.cancelChildren()
|
||||
// ответил первым: зеркало 2
|
||||
}
|
||||
```
|
||||
Важно: проигравший `Deferred` продолжает работу - его нужно отменить явно (здесь это делает `cancelChildren`).
|
||||
|
||||
## Channel как мьютекс для состояния
|
||||
Канал-«актор» - способ изолировать изменяемое состояние в одной корутине:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
sealed interface Command {
|
||||
data class Increment(val by: Int) : Command
|
||||
data class Read(val reply: CompletableDeferred<Int>) : Command
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val commands = Channel<Command>()
|
||||
|
||||
launch {
|
||||
var state = 0 // доступно только этой корутине
|
||||
for (cmd in commands) {
|
||||
when (cmd) {
|
||||
is Command.Increment -> state += cmd.by
|
||||
is Command.Read -> cmd.reply.complete(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repeat(100) { commands.send(Command.Increment(1)) }
|
||||
val reply = CompletableDeferred<Int>()
|
||||
commands.send(Command.Read(reply))
|
||||
println("состояние без блокировок: ${reply.await()}")
|
||||
commands.close()
|
||||
// состояние без блокировок: 100
|
||||
}
|
||||
```
|
||||
Синхронизации нет вообще: состояние изменяет одна корутина, остальные присылают сообщения. Подробнее: [Разделяемое состояние](Разделяемое%20состояние.md)
|
||||
|
||||
## Channel против Flow
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
// Channel: горячий, значения расходятся по получателям
|
||||
val channel = Channel<Int>(capacity = 3)
|
||||
channel.send(1); channel.send(2)
|
||||
channel.close()
|
||||
println("канал раздал: ${channel.toList()}")
|
||||
|
||||
// Flow: холодный, каждый подписчик получает всё
|
||||
val flow = flowOf(1, 2)
|
||||
println("поток повторил: ${flow.toList()} ${flow.toList()}")
|
||||
// канал раздал: [1, 2]
|
||||
// поток повторил: [1, 2] [1, 2]
|
||||
}
|
||||
```
|
||||
| | `Channel` | `Flow` |
|
||||
|---|---|---|
|
||||
| природа | горячий, объект-очередь | холодный, описание |
|
||||
| значение получает | один получатель | каждый подписчик |
|
||||
| существует без получателей | да, накапливает | нет, не запущен |
|
||||
| нужно закрывать | да | нет |
|
||||
| операторы | почти нет | десятки |
|
||||
| типичное применение | пул воркеров, развязка корутин | данные, события, UI |
|
||||
|
||||
Практическое правило: **начинайте с `Flow`**. Канал нужен, когда значения должны распределяться между получателями (пул воркеров), когда производитель и потребитель должны работать независимо, или когда нужна точная политика буферизации. Для «потока данных из репозитория в UI» канал - неправильный выбор: он одноразовый и легко теряет данные.
|
||||
|
||||
Если канал уже есть, его можно превратить во `Flow`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val channel = Channel<Int>(capacity = 2)
|
||||
channel.send(1); channel.send(2)
|
||||
channel.close()
|
||||
|
||||
println(channel.consumeAsFlow().toList()) // [1, 2]
|
||||
}
|
||||
```
|
||||
`consumeAsFlow()` можно собрать только один раз - канал одноразовый. Повторный `collect` бросит `IllegalStateException`.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `BlockingQueue<T>` | `Channel<T>` |
|
||||
> | `ArrayBlockingQueue(n)` | `Channel(capacity = n)` |
|
||||
> | `LinkedBlockingQueue()` | `Channel(UNLIMITED)` |
|
||||
> | `SynchronousQueue` | `Channel(RENDEZVOUS)` |
|
||||
> | `queue.put(x)` (блокирует поток) | `channel.send(x)` (приостанавливает) |
|
||||
> | `queue.take()` (блокирует поток) | `channel.receive()` (приостанавливает) |
|
||||
> | «отравленная пилюля» как признак конца | `close()` |
|
||||
> | `java.util.concurrent.Flow` | `Flow` |
|
||||
>
|
||||
> **`BlockingQueue` блокирует поток, канал - нет.** Это главное различие. Пул воркеров на `BlockingQueue` требует по потоку на воркера, и 1000 воркеров - это 1000 потоков. Канал держит их как корутины:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.channels.*
|
||||
> import java.util.concurrent.atomic.AtomicInteger
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val tasks = Channel<Int>(capacity = 100)
|
||||
> val done = AtomicInteger(0)
|
||||
> repeat(1000) { // 1000 воркеров
|
||||
> launch { for (t in tasks) { delay(1); done.incrementAndGet() } }
|
||||
> }
|
||||
> repeat(500) { tasks.send(it) }
|
||||
> tasks.close()
|
||||
> delay(200)
|
||||
> println("обработано: ${done.get()}") // обработано: 500
|
||||
> }
|
||||
> ```
|
||||
> В Java это 1000 потоков и около гигабайта стеков.
|
||||
>
|
||||
> **Признак конца данных.** `BlockingQueue` не умеет закрываться: чтобы сообщить воркерам «задач больше нет», в очередь кладут специальное значение - «отравленную пилюлю» (`POISON_PILL`), причём по одной на каждого воркера, иначе часть из них зависнет навсегда:
|
||||
> ```java
|
||||
> for (int i = 0; i < workers; i++) queue.put(POISON_PILL);
|
||||
> ```
|
||||
> Это хрупкий паттерн: нужно знать точное число получателей, а сама пилюля ломает типизацию (обычно это `null`-объект или специальный экземпляр). `channel.close()` корректно завершает всех получателей, сколько бы их ни было, а `for (x in channel)` просто выходит из цикла.
|
||||
>
|
||||
> **Передача ошибки.** В Java исключение из производителя в очередь не положить - его оборачивают в элемент-обёртку (`Either`-подобный класс) или передают через отдельный механизм. `channel.close(exception)` доставляет исключение получателям, и оно выбрасывается в точке `receive`.
|
||||
>
|
||||
> **Таймауты и выбор из нескольких очередей.** `BlockingQueue` даёт `poll(timeout, unit)`, но ждать «первое значение из двух очередей» штатно невозможно - приходится опрашивать по очереди с короткими таймаутами. `select` в Kotlin решает это напрямую, причём без активного ожидания.
|
||||
>
|
||||
> **`java.util.concurrent.Flow` - это Reactive Streams, а не очередь.** Оно появилось в Java 9 как SPI для реактивных библиотек, с ручным управлением давлением через `Subscription.request(n)`. Ближайший аналог в Kotlin - `Flow`, а не `Channel`. Подробнее: [Flow](Flow.md)
|
||||
>
|
||||
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Незакрытый канал подвешивает получателя навсегда:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.channels.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val channel = Channel<Int>()
|
||||
> launch {
|
||||
> channel.send(1)
|
||||
> // close() забыли
|
||||
> }
|
||||
> val job = launch {
|
||||
> for (v in channel) println("получено: $v")
|
||||
> println("этого не будет")
|
||||
> }
|
||||
> delay(50)
|
||||
> println("получатель всё ещё ждёт: ${job.isActive}")
|
||||
> coroutineContext.cancelChildren()
|
||||
> // получено: 1
|
||||
> // получатель всё ещё ждёт: true
|
||||
> }
|
||||
> ```
|
||||
> `for`-цикл по каналу ждёт `close()`. Используйте `produce`, который закрывает канал сам.
|
||||
>
|
||||
> **`receive()` на закрытом канале бросает исключение:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.channels.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val channel = Channel<Int>()
|
||||
> channel.close()
|
||||
> val error = runCatching { channel.receive() }.exceptionOrNull()
|
||||
> println(error is ClosedReceiveChannelException) // true
|
||||
> }
|
||||
> ```
|
||||
> Для безопасного чтения - `receiveCatching()` или `for`-цикл.
|
||||
>
|
||||
> **`send` на закрытом канале тоже бросает:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.channels.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val channel = Channel<Int>()
|
||||
> channel.close()
|
||||
> val error = runCatching { channel.send(1) }.exceptionOrNull()
|
||||
> println(error is ClosedSendChannelException) // true
|
||||
> }
|
||||
> ```
|
||||
> `trySend` вместо исключения вернёт неуспешный результат.
|
||||
>
|
||||
> **`Channel.UNLIMITED` - это `OutOfMemoryError` под нагрузкой.** Он снимает обратное давление: производитель быстрее потребителя будет накапливать элементы в памяти, пока процесс не упадёт. Безлимитный канал оправдан только при известном конечном числе элементов.
|
||||
>
|
||||
> **`trySend` молча теряет данные:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.channels.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val channel = Channel<Int>(capacity = 2)
|
||||
> val sent = (1..5).count { channel.trySend(it).isSuccess }
|
||||
> println("отправлено из 5: $sent") // отправлено из 5: 2
|
||||
> }
|
||||
> ```
|
||||
> Результат `trySend` обычно игнорируют - и три элемента исчезают без следа. Проверяйте `isSuccess` или используйте `send`.
|
||||
>
|
||||
> **Канал - одноразовый.** Повторный `consumeAsFlow().collect` или второй `toList()` после исчерпания бросит исключение либо вернёт пустой результат. Для многократного чтения нужен `Flow` или `SharedFlow`.
|
||||
>
|
||||
> **Канал раздаёт, а не транслирует.** Если нужно, чтобы **все** подписчики получили каждое событие, канал не подойдёт - элемент достаётся одному получателю. Нужен `SharedFlow`. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md)
|
||||
>
|
||||
> **`CONFLATED` теряет значения по определению.** Он хранит только последнее непрочитанное - для счётчиков и журналов это ошибка, для «актуального состояния» - норма.
|
||||
>
|
||||
> **Отмена корутины с `send` оставляет элемент непереданным:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.channels.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val channel = Channel<Int>()
|
||||
> val sender = launch { channel.send(1) } // ждёт получателя
|
||||
> delay(20)
|
||||
> sender.cancel()
|
||||
> println("отправитель отменён: ${sender.isCancelled}")
|
||||
> println("канал пуст: ${channel.tryReceive().isFailure}")
|
||||
> // отправитель отменён: true
|
||||
> // канал пуст: true
|
||||
> }
|
||||
> ```
|
||||
> Элемент не доставлен и не сохранён. Для надёжной доставки нужен буфер или подтверждение.
|
||||
>
|
||||
> **Проигравший в `select` продолжает работать.** `select` возвращает первый результат, но остальные `Deferred` и корутины живут дальше, расходуя ресурсы. Отменяйте их явно.
|
||||
>
|
||||
> **`select` с `onSend` и несколькими каналами не атомарен между итерациями.** В цикле состояние каналов между проверками меняется - на `select` нельзя строить транзакционную логику.
|
||||
>
|
||||
> **`produce` и `actor` помечены устаревшими** (`@ObsoleteCoroutinesApi`): их API может измениться. Для новых источников данных предпочтительнее `Flow`, для состояния - `Mutex` или `StateFlow`.
|
||||
>
|
||||
> **`isClosedForReceive`/`isEmpty` - гонка по определению.** Между проверкой и действием состояние изменится. Единственный корректный способ - `receiveCatching()` или `for`-цикл, обрабатывающие закрытие атомарно.
|
||||
|
||||
Далее: [Разделяемое состояние](Разделяемое%20состояние.md)
|
||||
@@ -0,0 +1,783 @@
|
||||
---
|
||||
title: Flow
|
||||
description: Холодный асинхронный поток значений: построение, операторы, контекст, обработка ошибок и backpressure
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
- flow
|
||||
---
|
||||
|
||||
`Flow<T>` - асинхронная последовательность значений. Если `suspend`-функция возвращает одно значение, то `Flow` - много, причём каждое может приходить с задержкой.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun temperatures(): Flow<Int> = flow {
|
||||
for (t in listOf(20, 22, 25)) {
|
||||
delay(20) // внутри flow можно приостанавливаться
|
||||
emit(t)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
temperatures().collect { println("температура: $it") }
|
||||
// температура: 20
|
||||
// температура: 22
|
||||
// температура: 25
|
||||
}
|
||||
```
|
||||
Сравните с `List`: список нужно вычислить целиком, прежде чем вернуть. `Flow` отдаёт значения по мере готовности.
|
||||
|
||||
## Холодный поток
|
||||
`Flow` ничего не делает, пока его не начнут собирать. Каждый вызов `collect` запускает тело заново.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
var starts = 0
|
||||
val f = flow {
|
||||
starts++
|
||||
emit(1)
|
||||
emit(2)
|
||||
}
|
||||
|
||||
println("создан, запусков: $starts") // создан, запусков: 0
|
||||
f.collect { }
|
||||
f.collect { }
|
||||
println("после двух collect: $starts") // после двух collect: 2
|
||||
}
|
||||
```
|
||||
Это главное свойство: `Flow` - **описание** вычисления, а не запущенный процесс. Горячие потоки, которые работают независимо от подписчиков, - это `StateFlow` и `SharedFlow`. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md)
|
||||
|
||||
## Способы создать Flow
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flowOf(1, 2, 3).collect { print("$it ") }
|
||||
println() // 1 2 3
|
||||
|
||||
listOf("a", "b").asFlow().collect { print("$it ") }
|
||||
println() // a b
|
||||
|
||||
(1..3).asFlow().collect { print("$it ") }
|
||||
println() // 1 2 3
|
||||
|
||||
flow {
|
||||
emit("из билдера")
|
||||
emitAll(flowOf("и", "вложенного"))
|
||||
}.collect { print("$it ") }
|
||||
println() // из билдера и вложенного
|
||||
}
|
||||
```
|
||||
Для колбэчных источников есть `callbackFlow` - он же мост от слушателей к `Flow`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
class Sensor {
|
||||
private var listener: ((Int) -> Unit)? = null
|
||||
fun subscribe(l: (Int) -> Unit) { listener = l }
|
||||
fun unsubscribe() { listener = null; println("слушатель снят") }
|
||||
fun fire(value: Int) = listener?.invoke(value)
|
||||
}
|
||||
|
||||
fun Sensor.readings(): Flow<Int> = callbackFlow {
|
||||
subscribe { value -> trySend(value) }
|
||||
awaitClose { unsubscribe() } // вызовется при отмене collect
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val sensor = Sensor()
|
||||
val job = launch {
|
||||
sensor.readings().collect { println("показание: $it") }
|
||||
}
|
||||
delay(20)
|
||||
sensor.fire(42)
|
||||
sensor.fire(43)
|
||||
delay(20)
|
||||
job.cancelAndJoin()
|
||||
// показание: 42
|
||||
// показание: 43
|
||||
// слушатель снят
|
||||
}
|
||||
```
|
||||
`awaitClose` обязателен: без него `callbackFlow` бросит `IllegalStateException`, потому что иначе слушатель остался бы висеть.
|
||||
|
||||
## Промежуточные операторы
|
||||
Операторы ленивые: они только оборачивают поток, не запуская его.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val result = (1..10).asFlow()
|
||||
.filter { it % 2 == 0 }
|
||||
.map { it * it }
|
||||
.take(3)
|
||||
.toList()
|
||||
println(result) // [4, 16, 36]
|
||||
}
|
||||
```
|
||||
Главное отличие от операторов коллекций - внутри можно приостанавливаться:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
suspend fun enrich(id: Int): String { delay(10); return "запись $id" }
|
||||
|
||||
fun main() = runBlocking {
|
||||
flowOf(1, 2, 3)
|
||||
.map { enrich(it) } // suspend внутри map
|
||||
.collect { println(it) }
|
||||
// запись 1
|
||||
// запись 2
|
||||
// запись 3
|
||||
}
|
||||
```
|
||||
|
||||
| Оператор | Что делает |
|
||||
|---|---|
|
||||
| `map`, `filter` | как у коллекций, но с `suspend` внутри |
|
||||
| `transform` | произвольное число `emit` на элемент |
|
||||
| `take`, `drop`, `takeWhile` | ограничение по количеству |
|
||||
| `onEach` | побочный эффект без изменения значений |
|
||||
| `withIndex`, `runningFold`, `scan` | накопление |
|
||||
| `distinctUntilChanged` | пропуск подряд идущих дублей |
|
||||
| `debounce`, `sample` | фильтрация по времени |
|
||||
| `flatMapConcat`, `flatMapMerge`, `flatMapLatest` | поток из потоков |
|
||||
| `zip`, `combine` | объединение двух потоков |
|
||||
| `onStart`, `onCompletion`, `catch` | обработка жизненного цикла |
|
||||
|
||||
`transform` - самый общий:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flowOf(1, 2, 3)
|
||||
.transform { value ->
|
||||
emit("начало $value")
|
||||
emit("конец $value")
|
||||
}
|
||||
.collect { print("$it; ") }
|
||||
println()
|
||||
// начало 1; конец 1; начало 2; конец 2; начало 3; конец 3;
|
||||
}
|
||||
```
|
||||
|
||||
## Терминальные операторы
|
||||
Только они запускают поток, и все они `suspend`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val f = flowOf(1, 2, 3, 4)
|
||||
|
||||
println(f.toList()) // [1, 2, 3, 4]
|
||||
println(f.first()) // 1
|
||||
println(f.firstOrNull { it > 10 }) // null
|
||||
println(f.lastOrNull()) // 4
|
||||
println(f.count()) // 4
|
||||
println(f.reduce { a, b -> a + b }) // 10
|
||||
println(f.fold(100) { a, b -> a + b }) // 110
|
||||
println(f.toSet()) // [1, 2, 3, 4]
|
||||
println(f.filter { it == 2 }.single()) // 2
|
||||
}
|
||||
```
|
||||
`first()` отменяет поток сразу после первого значения - этого достаточно, чтобы остановить бесконечный источник:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
var emitted = 0
|
||||
val infinite = flow {
|
||||
var i = 0
|
||||
while (true) {
|
||||
emitted++
|
||||
emit(i++)
|
||||
}
|
||||
}
|
||||
println(infinite.first()) // 0
|
||||
println("излучено значений: $emitted") // излучено значений: 1
|
||||
}
|
||||
```
|
||||
|
||||
## Контекст и flowOn
|
||||
По умолчанию `Flow` выполняется в контексте того, кто его собирает - это называется context preservation:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flow {
|
||||
println("flow на: ${Thread.currentThread().name}")
|
||||
emit(1)
|
||||
}.collect {
|
||||
println("collect на: ${Thread.currentThread().name}")
|
||||
}
|
||||
// flow на: main
|
||||
// collect на: main
|
||||
}
|
||||
```
|
||||
Сменить контекст **производителя** можно только через `flowOn`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flow {
|
||||
val onPool = Thread.currentThread().name.contains("DefaultDispatcher")
|
||||
println("flow в пуле: $onPool")
|
||||
emit(1)
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.collect {
|
||||
println("collect на: ${Thread.currentThread().name}")
|
||||
}
|
||||
// flow в пуле: true
|
||||
// collect на: main
|
||||
}
|
||||
```
|
||||
`flowOn` действует на всё, что **выше** него по цепочке:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flowOf(1, 2)
|
||||
.map { it * 10 } // в IO
|
||||
.flowOn(Dispatchers.IO)
|
||||
.map { it + 1 } // в main
|
||||
.collect { print("$it ") }
|
||||
println() // 11 21
|
||||
}
|
||||
```
|
||||
Менять контекст внутри `flow { }` через `withContext` запрещено:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val broken = flow {
|
||||
withContext(Dispatchers.IO) { emit(1) } // нарушение контракта
|
||||
}
|
||||
val error = runCatching { broken.collect { } }.exceptionOrNull()
|
||||
println(error is IllegalStateException) // true
|
||||
println(error?.message?.contains("Flow invariant is violated")) // true
|
||||
}
|
||||
```
|
||||
Причина запрета - предсказуемость: собирающий код должен быть уверен, что `collect` вызывается в его контексте (например, в UI-потоке). Для смены контекста есть только `flowOn`.
|
||||
|
||||
## Обработка ошибок
|
||||
`try/catch` работает, но правильнее оператор `catch`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flow {
|
||||
emit(1)
|
||||
throw IllegalStateException("источник упал")
|
||||
}
|
||||
.catch { e -> println("перехвачено: ${e.message}") }
|
||||
.collect { println("значение: $it") }
|
||||
// значение: 1
|
||||
// перехвачено: источник упал
|
||||
}
|
||||
```
|
||||
`catch` ловит ошибки только **выше** себя - это называется прозрачность исключений:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val error = runCatching {
|
||||
flowOf(1)
|
||||
.catch { println("не сработает") }
|
||||
.collect { throw IllegalStateException("ошибка в collect") }
|
||||
}.exceptionOrNull()
|
||||
println("ошибка collect прошла мимо catch: ${error?.message}")
|
||||
// ошибка collect прошла мимо catch: ошибка в collect
|
||||
}
|
||||
```
|
||||
Чтобы ловить и ошибки потребителя, нужен `onEach` + `catch` в таком порядке:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flowOf(1, 2)
|
||||
.onEach { if (it == 2) error("обработка упала на $it") }
|
||||
.catch { e -> println("перехвачено: ${e.message}") }
|
||||
.collect { println("обработано: $it") }
|
||||
// обработано: 1
|
||||
// перехвачено: обработка упала на 2
|
||||
}
|
||||
```
|
||||
`catch` может подменить поток значениями по умолчанию:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flow<String> { error("сеть недоступна") }
|
||||
.catch { emit("данные из кеша") }
|
||||
.collect { println(it) }
|
||||
// данные из кеша
|
||||
}
|
||||
```
|
||||
|
||||
## retry и повторные попытки
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
var attempt = 0
|
||||
flow {
|
||||
attempt++
|
||||
if (attempt < 3) error("попытка $attempt не удалась")
|
||||
emit("успех на попытке $attempt")
|
||||
}
|
||||
.retry(3) { e ->
|
||||
println("ретрай после: ${e.message}")
|
||||
delay(10)
|
||||
true
|
||||
}
|
||||
.collect { println(it) }
|
||||
// ретрай после: попытка 1 не удалась
|
||||
// ретрай после: попытка 2 не удалась
|
||||
// успех на попытке 3
|
||||
}
|
||||
```
|
||||
`retryWhen` даёт доступ к номеру попытки - это нужно для экспоненциальной задержки:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
var calls = 0
|
||||
flow {
|
||||
calls++
|
||||
if (calls < 3) error("сбой")
|
||||
emit("готово")
|
||||
}
|
||||
.retryWhen { _, attempt ->
|
||||
if (attempt >= 3) false
|
||||
else {
|
||||
delay(10L * (attempt + 1)) // 10, 20, 30 мс
|
||||
println("попытка ${attempt + 1}")
|
||||
true
|
||||
}
|
||||
}
|
||||
.collect { println(it) }
|
||||
// попытка 1
|
||||
// попытка 2
|
||||
// готово
|
||||
}
|
||||
```
|
||||
|
||||
## Жизненный цикл: onStart, onCompletion
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flowOf(1, 2)
|
||||
.onStart { println("начали") }
|
||||
.onEach { println("значение $it") }
|
||||
.onCompletion { cause -> println("завершили, ошибка: $cause") }
|
||||
.collect { }
|
||||
// начали
|
||||
// значение 1
|
||||
// значение 2
|
||||
// завершили, ошибка: null
|
||||
}
|
||||
```
|
||||
`onCompletion` вызывается всегда - при успехе, ошибке и отмене, причём получает причину:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flow {
|
||||
emit(1)
|
||||
error("упало")
|
||||
}
|
||||
.onCompletion { cause -> println("причина: ${cause?.message}") }
|
||||
.catch { }
|
||||
.collect { }
|
||||
// причина: упало
|
||||
}
|
||||
```
|
||||
`onStart` может излучать значения - это способ показать «загрузка...» до первого реального элемента:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
flow { delay(20); emit("данные") }
|
||||
.onStart { emit("загрузка...") }
|
||||
.collect { println(it) }
|
||||
// загрузка...
|
||||
// данные
|
||||
}
|
||||
```
|
||||
|
||||
## buffer: производитель и потребитель параллельно
|
||||
По умолчанию производитель ждёт потребителя:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun source() = flow {
|
||||
repeat(3) {
|
||||
delay(20) // производство
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val sequential = measureTimeMillis {
|
||||
source().collect { delay(20) } // потребление
|
||||
}
|
||||
val buffered = measureTimeMillis {
|
||||
source().buffer().collect { delay(20) }
|
||||
}
|
||||
println("без буфера около 120 мс: ${sequential >= 110}")
|
||||
println("с буфером быстрее: ${buffered < sequential}")
|
||||
// без буфера около 120 мс: true
|
||||
// с буфером быстрее: true
|
||||
}
|
||||
```
|
||||
Без буфера шаги складываются: 3 × (20 + 20) = 120 мс. С буфером производство идёт параллельно с потреблением, и общее время приближается к максимуму из двух конвейеров.
|
||||
|
||||
## conflate и collectLatest
|
||||
Когда важны только свежие значения, промежуточные можно отбрасывать:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun fast() = flow {
|
||||
repeat(5) {
|
||||
delay(10)
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val all = mutableListOf<Int>()
|
||||
fast().collect { delay(40); all += it }
|
||||
|
||||
val conflated = mutableListOf<Int>()
|
||||
fast().conflate().collect { delay(40); conflated += it }
|
||||
|
||||
println("без conflate получено всё: ${all.size == 5}")
|
||||
println("с conflate пропущены промежуточные: ${conflated.size < 5}")
|
||||
println("последнее значение всегда доходит: ${conflated.last() == 4}")
|
||||
// без conflate получено всё: true
|
||||
// с conflate пропущены промежуточные: true
|
||||
// последнее значение всегда доходит: true
|
||||
}
|
||||
```
|
||||
`collectLatest` работает иначе: он не пропускает значения, а **отменяет** обработку предыдущего:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val completed = mutableListOf<Int>()
|
||||
flow {
|
||||
repeat(3) { delay(10); emit(it) }
|
||||
}.collectLatest { value ->
|
||||
delay(25) // не успеет для первых двух
|
||||
completed += value
|
||||
}
|
||||
println("доработал только последний: $completed") // доработал только последний: [2]
|
||||
}
|
||||
```
|
||||
|
||||
| Стратегия | Поведение при быстром производителе |
|
||||
|---|---|
|
||||
| по умолчанию | производитель ждёт потребителя |
|
||||
| `buffer(n)` | очередь на `n` элементов, затем ожидание |
|
||||
| `buffer(onBufferOverflow = DROP_OLDEST)` | выбрасывает старые |
|
||||
| `conflate()` | хранит только последнее непрочитанное |
|
||||
| `collectLatest` | отменяет обработку устаревшего значения |
|
||||
| `debounce(ms)` | пропускает только после паузы |
|
||||
| `sample(ms)` | берёт последнее раз в интервал |
|
||||
|
||||
## Объединение потоков
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val letters = flowOf("a", "b", "c")
|
||||
val numbers = flowOf(1, 2, 3)
|
||||
|
||||
println(letters.zip(numbers) { l, n -> "$l$n" }.toList()) // [a1, b2, c3]
|
||||
|
||||
val slow = flow { emit(1); delay(30); emit(2) }
|
||||
val fast2 = flow { emit("x"); delay(10); emit("y") }
|
||||
println(slow.combine(fast2) { a, b -> "$a$b" }.toList())
|
||||
// [1x, 1y, 2y]
|
||||
}
|
||||
```
|
||||
`zip` ждёт пару - элементов будет столько, сколько в коротком потоке. `combine` излучает при каждом изменении любого из потоков, используя последние известные значения другого. Для UI (например, «форма валидна, если валидны все поля») нужен `combine`.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val concatenated = flowOf(1, 2).flatMapConcat { n ->
|
||||
flow { emit("$n-a"); delay(10); emit("$n-b") }
|
||||
}.toList()
|
||||
println(concatenated) // [1-a, 1-b, 2-a, 2-b]
|
||||
|
||||
val latest = flowOf(1, 2, 3).flatMapLatest { n ->
|
||||
flow { delay(20); emit("результат $n") }
|
||||
}.toList()
|
||||
println(latest) // [результат 3]
|
||||
}
|
||||
```
|
||||
`flatMapLatest` - основной оператор для поиска по вводу: новый запрос отменяет предыдущий.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
suspend fun search(query: String): List<String> {
|
||||
delay(20)
|
||||
return listOf("результат для «$query»")
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val input = flow {
|
||||
emit("к"); delay(5)
|
||||
emit("ко"); delay(5)
|
||||
emit("кот"); delay(100) // пользователь остановился
|
||||
emit("кота")
|
||||
}
|
||||
|
||||
input
|
||||
.debounce(30) // ждём паузу в наборе
|
||||
.filter { it.length >= 2 }
|
||||
.flatMapLatest { search(it).asFlow() }
|
||||
.collect { println(it) }
|
||||
// результат для «кот»
|
||||
// результат для «кота»
|
||||
}
|
||||
```
|
||||
Это каноничный конвейер: `debounce` отсекает промежуточные нажатия, `flatMapLatest` отменяет устаревший запрос.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["ввод: к, ко, кот"] --> B["debounce(30)"]
|
||||
B --> C["только 'кот'<br/>после паузы"]
|
||||
C --> D["flatMapLatest"]
|
||||
D --> E["запрос к API"]
|
||||
E --> F["collect: результаты"]
|
||||
D -.->|"пришёл новый ввод"| G["отмена<br/>предыдущего запроса"]
|
||||
```
|
||||
|
||||
## Flow как возвращаемый тип
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
class OrderRepository {
|
||||
// не suspend: Flow холодный, вызов ничего не запускает
|
||||
fun observeOrders(): Flow<String> = flow {
|
||||
repeat(3) { i ->
|
||||
delay(10)
|
||||
emit("заказ $i")
|
||||
}
|
||||
}.flowOn(Dispatchers.IO) // производитель всегда в IO
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
OrderRepository().observeOrders().collect { println(it) }
|
||||
// заказ 0
|
||||
// заказ 1
|
||||
// заказ 2
|
||||
}
|
||||
```
|
||||
Правила проектирования: функция, возвращающая `Flow`, не должна быть `suspend` (создание потока мгновенно), а `flowOn` ставится внутри - вызывающий не обязан знать, где работает источник.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `Stream<T>` (синхронный, одноразовый) | `Sequence<T>` |
|
||||
> | `Flow` из Reactive Streams | `Flow<T>` |
|
||||
> | RxJava `Observable`/`Flowable` | `Flow<T>` |
|
||||
> | Project Reactor `Flux<T>` | `Flow<T>` |
|
||||
> | `Subscriber.request(n)` | приостановка `emit` |
|
||||
> | `subscribeOn`/`observeOn` | `flowOn` |
|
||||
> | `onErrorResume` | `catch` |
|
||||
> | `BlockingQueue` + поток | `Channel` |
|
||||
>
|
||||
> **`java.util.stream.Stream` - это не аналог.** `Stream` синхронный: `stream.map(f)` не может ждать ответа сети, а терминальная операция блокирует поток до конца обработки. Прямой аналог `Stream` в Kotlin - `Sequence`. `Flow` отличается тем, что внутри каждого оператора разрешена приостановка. Подробнее: [Sequence](../Коллекции/Sequence.md)
|
||||
>
|
||||
> **Reactive Streams решают ту же задачу, но сложнее.** RxJava и Reactor построены на коллбэках (`onNext`, `onError`, `onComplete`) и требуют явного управления давлением через `request(n)`. В `Flow` backpressure получается бесплатно: `emit` - это `suspend`-функция, поэтому если потребитель не готов, производитель просто приостанавливается:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> var produced = 0
|
||||
> flow {
|
||||
> repeat(100) { produced++; emit(it) }
|
||||
> }.take(3).collect { }
|
||||
> println("произведено ровно столько, сколько взяли: $produced")
|
||||
> // произведено ровно столько, сколько взяли: 3
|
||||
> }
|
||||
> ```
|
||||
> Никакого `request(n)` - producer сам останавливается на `emit`.
|
||||
>
|
||||
> **Стектрейсы и отладка.** В RxJava стектрейс ошибки состоит из кадров библиотеки, а место в бизнес-логике приходится искать по `onAssembly`-хукам. `Flow` - обычные `suspend`-функции, поэтому исключение приходит с кадрами вашего кода, а `try/catch`, `for` и `return` работают внутри операторов естественно.
|
||||
>
|
||||
> **Количество операторов.** В RxJava их несколько сотен (`flatMapIterable`, `concatMapDelayError`, `switchIfEmpty`...), потому что каждый сценарий требует отдельного оператора - внутри цепочки нельзя писать обычный код. В `Flow` операторов несколько десятков: остальное выражается через `transform` и обычные конструкции языка.
|
||||
>
|
||||
> **Отмена подписки.** В Rx нужно хранить `Disposable` и не забыть вызвать `dispose()` - классический источник утечек, для борьбы с которым придумали `CompositeDisposable`. `Flow` собирается внутри корутины, поэтому отмена области автоматически прекращает сбор:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch {
|
||||
> flow {
|
||||
> try {
|
||||
> repeat(100) { delay(10); emit(it) }
|
||||
> } finally { println("источник закрыт") }
|
||||
> }.collect { }
|
||||
> }
|
||||
> delay(30)
|
||||
> job.cancelAndJoin()
|
||||
> // источник закрыт
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Context preservation - то, чего нет в Rx.** В RxJava `observeOn` можно поставить в любом месте, и понять, на каком потоке выполнится `subscribe`, можно только прочитав всю цепочку. `Flow` гарантирует, что `collect` всегда выполняется в контексте собирающего, а `withContext` внутри `flow { }` запрещён на уровне рантайма.
|
||||
>
|
||||
> Сравнить: [Многопоточность в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`Flow` не запустится без терминального оператора:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> var side = 0
|
||||
> flowOf(1, 2, 3).onEach { side++ } // collect не вызван
|
||||
> delay(20)
|
||||
> println("побочных эффектов: $side") // побочных эффектов: 0
|
||||
> }
|
||||
> ```
|
||||
> Компилятор предупреждает о неиспользованном результате, но не всегда - например, если значение присвоено переменной.
|
||||
>
|
||||
> **`withContext` внутри `flow { }` бросает исключение в рантайме:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val error = runCatching {
|
||||
> flow { withContext(Dispatchers.IO) { emit(1) } }.collect { }
|
||||
> }.exceptionOrNull()
|
||||
> println(error?.message?.contains("Flow invariant is violated")) // true
|
||||
> }
|
||||
> ```
|
||||
> Это ошибка времени выполнения, а не компиляции - её легко пропустить в редкой ветке кода. Используйте `flowOn`.
|
||||
>
|
||||
> **`flowOn` действует только выше по цепочке.** `flow.map { a }.flowOn(IO).map { b }` выполнит `a` в IO, а `b` - в контексте сборщика. Порядок операторов меняет смысл.
|
||||
>
|
||||
> **`catch` не ловит ошибки `collect`:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val error = runCatching {
|
||||
> flowOf(1).catch { println("не вызовется") }.collect { error("из collect") }
|
||||
> }.exceptionOrNull()
|
||||
> println("ошибка прошла мимо: ${error?.message}") // ошибка прошла мимо: из collect
|
||||
> }
|
||||
> ```
|
||||
> Перенесите логику в `onEach` перед `catch`.
|
||||
>
|
||||
> **`emit` из другой корутины запрещён:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val broken = flow {
|
||||
> coroutineScope {
|
||||
> launch { emit(1) } // emit в другой корутине
|
||||
> }
|
||||
> }
|
||||
> val error = runCatching { broken.collect { } }.exceptionOrNull()
|
||||
> println(error != null) // true
|
||||
> }
|
||||
> ```
|
||||
> Сообщение - про нарушение инварианта. Для излучения из нескольких источников нужен `channelFlow` или `callbackFlow`.
|
||||
>
|
||||
> **`callbackFlow` без `awaitClose` падает:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val broken = callbackFlow<Int> {
|
||||
> trySend(1)
|
||||
> // нет awaitClose
|
||||
> }
|
||||
> val error = runCatching { broken.collect { } }.exceptionOrNull()
|
||||
> println(error is IllegalStateException) // true
|
||||
> }
|
||||
> ```
|
||||
> Это защита от утечки слушателя: без `awaitClose` библиотека не знает, когда отписываться.
|
||||
>
|
||||
> **`trySend` молча теряет значения при полном буфере.** У `callbackFlow` буфер по умолчанию 64 элемента; `trySend` возвращает `ChannelResult`, который обычно игнорируют. Если источник быстрее потребителя, часть событий исчезнет - проверяйте результат или задавайте `buffer(Channel.UNLIMITED)` осознанно.
|
||||
>
|
||||
> **Повторный `collect` запускает поток заново.** Если `Flow` делает сетевой запрос, два `collect` - это два запроса. Для разделения результата между подписчиками нужен `shareIn`/`stateIn`. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md)
|
||||
>
|
||||
> **`Flow` без буфера последователен, а не параллелен:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
> import kotlin.system.measureTimeMillis
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val t = measureTimeMillis {
|
||||
> flowOf(1, 2, 3).map { delay(30); it }.collect { }
|
||||
> }
|
||||
> println("последовательно, около 90 мс: ${t >= 85}") // последовательно, около 90 мс: true
|
||||
> }
|
||||
> ```
|
||||
> `map` с `suspend` внутри обрабатывает элементы по одному. Для параллельной обработки нужен `flatMapMerge` с `concurrency` или `map { async { } }` поверх списка.
|
||||
>
|
||||
> **`conflate` и `collectLatest` теряют данные - это их цель.** Для UI-состояния это правильно, для обработки платежей или записи в журнал - нет. Убедитесь, что потеря промежуточных значений допустима.
|
||||
>
|
||||
> **`debounce` задерживает даже последнее значение.** Пользователь перестал печатать - результат появится только через указанный интервал. Слишком большое значение ощущается как лаг интерфейса.
|
||||
>
|
||||
> **Бесконечный `Flow` в `toList()` повиснет.** Терминальные операторы, собирающие всё (`toList`, `count`, `fold`), на бесконечном источнике не завершатся никогда. Нужен `take(n)` или `first()`.
|
||||
>
|
||||
> **`SharingStarted.Eagerly` в `stateIn` держит источник активным всегда** - типичная утечка при переносе холодного `Flow` в горячий. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md)
|
||||
>
|
||||
> **Исключение в `onCompletion` не перехватывается предыдущим `catch`.** `onCompletion` - не `finally`: он не может подавить ошибку, а своя ошибка внутри него уйдёт потребителю.
|
||||
>
|
||||
> **`first()` отменяет источник.** Для `callbackFlow` это значит, что слушатель будет снят сразу после первого значения - если вы ожидали продолжения подписки, её нет.
|
||||
|
||||
Далее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md)
|
||||
@@ -0,0 +1,618 @@
|
||||
---
|
||||
title: StateFlow и SharedFlow
|
||||
description: Горячие потоки: хранение состояния, события, replay, stateIn и shareIn, конфляция и выбор между ними
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
- flow
|
||||
---
|
||||
|
||||
`StateFlow` и `SharedFlow` - **горячие** потоки: они существуют и работают независимо от подписчиков. Обычный `Flow` холодный - он запускается на каждый `collect` и умирает вместе с ним.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val state = MutableStateFlow(0)
|
||||
|
||||
val job = launch {
|
||||
state.collect { println("подписчик: $it") }
|
||||
}
|
||||
delay(20)
|
||||
state.value = 1
|
||||
state.value = 2
|
||||
delay(20)
|
||||
job.cancel()
|
||||
println("текущее значение без подписчиков: ${state.value}")
|
||||
// подписчик: 0
|
||||
// подписчик: 1
|
||||
// подписчик: 2
|
||||
// текущее значение без подписчиков: 2
|
||||
}
|
||||
```
|
||||
Обратите внимание: подписчик сразу получил текущее значение `0`, а после отмены сбора состояние осталось доступным через `.value`.
|
||||
|
||||
## StateFlow: состояние
|
||||
`StateFlow` - это наблюдаемое значение. У него всегда есть текущее состояние, и оно всегда одно.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
class Counter {
|
||||
private val _count = MutableStateFlow(0)
|
||||
val count: StateFlow<Int> = _count.asStateFlow() // только чтение снаружи
|
||||
|
||||
fun increment() { _count.value++ }
|
||||
fun add(n: Int) = _count.update { it + n } // атомарно
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val counter = Counter()
|
||||
val job = launch { counter.count.collect { println("счёт: $it") } }
|
||||
|
||||
delay(20)
|
||||
counter.increment()
|
||||
delay(20)
|
||||
counter.add(10)
|
||||
delay(20)
|
||||
job.cancel()
|
||||
// счёт: 0
|
||||
// счёт: 1
|
||||
// счёт: 11
|
||||
}
|
||||
```
|
||||
Пара «приватный `MutableStateFlow` + публичный `StateFlow`» - основной шаблон: изменять состояние может только владелец, читать - все.
|
||||
|
||||
## StateFlow конфлюирует и дедуплицирует
|
||||
Два свойства, которые нужно знать наизусть.
|
||||
|
||||
**1. Одинаковые подряд значения не доходят** - `StateFlow` сравнивает через `equals`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val state = MutableStateFlow("a")
|
||||
val received = mutableListOf<String>()
|
||||
val job = launch { state.collect { received += it } }
|
||||
|
||||
delay(20)
|
||||
state.value = "a"; delay(20) // то же значение — не придёт
|
||||
state.value = "b"; delay(20)
|
||||
state.value = "b"; delay(20) // снова дубль
|
||||
state.value = "a"; delay(20)
|
||||
job.cancel()
|
||||
println(received) // [a, b, a]
|
||||
}
|
||||
```
|
||||
Задержки между присваиваниями здесь не для красоты: без них подписчик не успел бы обработать значения и сработало бы второе свойство - конфляция (см. ниже), из-за которой он увидел бы только `a` и финальное значение.
|
||||
|
||||
Это бесплатный `distinctUntilChanged`, но он же источник ошибок с изменяемыми объектами:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
class MutableBox(val items: MutableList<Int>)
|
||||
|
||||
fun main() = runBlocking {
|
||||
val box = MutableBox(mutableListOf(1))
|
||||
val state = MutableStateFlow(box)
|
||||
var updates = 0
|
||||
val job = launch { state.collect { updates++ } }
|
||||
|
||||
delay(20)
|
||||
box.items.add(2) // изменили содержимое
|
||||
state.value = box // тот же объект — equals true
|
||||
delay(20)
|
||||
job.cancel()
|
||||
println("обновлений: $updates") // обновлений: 1
|
||||
}
|
||||
```
|
||||
Подписчик не узнал об изменении. Поэтому состояние в `StateFlow` должно быть неизменяемым - `data class` с `val` и неизменяемыми коллекциями. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
|
||||
|
||||
**2. Медленный подписчик пропускает промежуточные значения:**
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val state = MutableStateFlow(0)
|
||||
val received = mutableListOf<Int>()
|
||||
val job = launch {
|
||||
state.collect { delay(50); received += it }
|
||||
}
|
||||
delay(10)
|
||||
repeat(10) { state.value = it + 1 }
|
||||
delay(150)
|
||||
job.cancel()
|
||||
println("получено не всё: ${received.size < 10}")
|
||||
println("последнее значение дошло: ${received.last() == 10}")
|
||||
// получено не всё: true
|
||||
// последнее значение дошло: true
|
||||
}
|
||||
```
|
||||
`StateFlow` - про актуальное состояние, а не про историю. Последнее значение дойдёт всегда, промежуточные - нет.
|
||||
|
||||
## MutableStateFlow.update
|
||||
Присваивание `value` не атомарно при конкуренции:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val unsafe = MutableStateFlow(0)
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { launch { repeat(100) { unsafe.value = unsafe.value + 1 } } }
|
||||
.forEach { it.join() }
|
||||
}
|
||||
println("потеряны обновления: ${unsafe.value < 10_000}")
|
||||
|
||||
val safe = MutableStateFlow(0)
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { launch { repeat(100) { safe.update { v -> v + 1 } } } }
|
||||
.forEach { it.join() }
|
||||
}
|
||||
println("update атомарен: ${safe.value == 10_000}")
|
||||
// потеряны обновления: true
|
||||
// update атомарен: true
|
||||
}
|
||||
```
|
||||
`update` выполняет CAS-цикл, поэтому при конкурентных изменениях нужен именно он. Подробнее: [Разделяемое состояние](Разделяемое%20состояние.md)
|
||||
|
||||
## SharedFlow: события
|
||||
`SharedFlow` не хранит «текущее значение» - он транслирует события всем подписчикам.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val events = MutableSharedFlow<String>()
|
||||
|
||||
val a = launch { events.collect { println("A: $it") } }
|
||||
val b = launch { events.collect { println("B: $it") } }
|
||||
delay(20) // дать подписчикам стартовать
|
||||
|
||||
events.emit("событие 1")
|
||||
delay(20)
|
||||
a.cancel(); b.cancel()
|
||||
// A: событие 1
|
||||
// B: событие 1
|
||||
}
|
||||
```
|
||||
По умолчанию `replay = 0`: подписчик получает только то, что произошло **после** подписки. Событие, отправленное без подписчиков, исчезает:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val events = MutableSharedFlow<String>()
|
||||
events.emit("в пустоту") // подписчиков нет
|
||||
|
||||
val received = mutableListOf<String>()
|
||||
val job = launch { events.collect { received += it } }
|
||||
delay(20)
|
||||
events.emit("услышано")
|
||||
delay(20)
|
||||
job.cancel()
|
||||
println(received) // [услышано]
|
||||
}
|
||||
```
|
||||
|
||||
## replay
|
||||
`replay` задаёт, сколько последних событий получит новый подписчик:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val events = MutableSharedFlow<Int>(replay = 2)
|
||||
events.emit(1)
|
||||
events.emit(2)
|
||||
events.emit(3)
|
||||
|
||||
val received = mutableListOf<Int>()
|
||||
val job = launch { events.collect { received += it } }
|
||||
delay(20)
|
||||
job.cancel()
|
||||
println(received) // [2, 3]
|
||||
}
|
||||
```
|
||||
`StateFlow` эквивалентен `SharedFlow(replay = 1)` с дедупликацией и обязательным начальным значением.
|
||||
|
||||
## Буфер и стратегия переполнения
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val dropping = MutableSharedFlow<Int>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 2,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
val received = mutableListOf<Int>()
|
||||
val job = launch { dropping.collect { delay(50); received += it } }
|
||||
delay(20)
|
||||
|
||||
repeat(10) { dropping.emit(it) } // emit не приостанавливается
|
||||
delay(200)
|
||||
job.cancel()
|
||||
println("часть событий отброшена: ${received.size < 10}") // часть событий отброшена: true
|
||||
}
|
||||
```
|
||||
| Параметр | Смысл |
|
||||
|---|---|
|
||||
| `replay` | сколько событий повторить новому подписчику |
|
||||
| `extraBufferCapacity` | буфер сверх `replay` |
|
||||
| `onBufferOverflow` | `SUSPEND` (по умолчанию), `DROP_OLDEST`, `DROP_LATEST` |
|
||||
|
||||
Важно: `DROP_OLDEST`/`DROP_LATEST` работают только при ненулевом буфере (`replay + extraBufferCapacity > 0`).
|
||||
|
||||
`tryEmit` - неприостанавливающаяся отправка:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val noBuffer = MutableSharedFlow<Int>()
|
||||
println("без буфера и подписчиков: ${noBuffer.tryEmit(1)}") // без буфера и подписчиков: true
|
||||
|
||||
val withSubscriber = MutableSharedFlow<Int>()
|
||||
val job = launch { withSubscriber.collect { delay(1000) } }
|
||||
delay(20)
|
||||
println("подписчик занят, буфера нет: ${withSubscriber.tryEmit(2)}") // подписчик занят, буфера нет: false
|
||||
job.cancel()
|
||||
}
|
||||
```
|
||||
Тонкость, которая удивляет: без подписчиков `tryEmit` возвращает `true`, хотя событие никто не получил - отправка «удалась», получателей просто не было.
|
||||
|
||||
## stateIn и shareIn
|
||||
Превращают холодный `Flow` в горячий:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
var starts = 0
|
||||
val cold = flow {
|
||||
starts++
|
||||
repeat(3) { delay(20); emit(it) }
|
||||
}
|
||||
|
||||
val hot = cold.stateIn(
|
||||
scope = this,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = -1,
|
||||
)
|
||||
delay(30)
|
||||
|
||||
val a = launch { hot.take(1).collect { println("A видит: $it") } }
|
||||
val b = launch { hot.take(1).collect { println("B видит: $it") } }
|
||||
a.join(); b.join()
|
||||
println("источник запущен раз: $starts")
|
||||
// A видит: 0
|
||||
// B видит: 0
|
||||
// источник запущен раз: 1
|
||||
}
|
||||
```
|
||||
Без `stateIn` два подписчика запустили бы источник дважды - два сетевых запроса вместо одного.
|
||||
|
||||
`SharingStarted` определяет жизненный цикл источника:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
var active = 0
|
||||
val source = flow {
|
||||
active++
|
||||
try {
|
||||
repeat(100) { delay(10); emit(it) }
|
||||
} finally { active-- }
|
||||
}
|
||||
|
||||
val shared = source.shareIn(
|
||||
scope = this,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
replay = 1,
|
||||
)
|
||||
|
||||
val job = launch { shared.collect { } }
|
||||
delay(30)
|
||||
println("источник активен при подписчике: ${active == 1}")
|
||||
job.cancelAndJoin()
|
||||
delay(30)
|
||||
println("источник остановлен без подписчиков: ${active == 0}")
|
||||
coroutineContext.cancelChildren() // иначе runBlocking ждёт корутину shareIn
|
||||
// источник активен при подписчике: true
|
||||
// источник остановлен без подписчиков: true
|
||||
}
|
||||
```
|
||||
| `SharingStarted` | Когда источник активен |
|
||||
|---|---|
|
||||
| `Eagerly` | сразу и до отмены scope |
|
||||
| `Lazily` | с первого подписчика, затем навсегда |
|
||||
| `WhileSubscribed()` | только пока есть подписчики |
|
||||
| `WhileSubscribed(stopTimeoutMillis = 5000)` | плюс пауза перед остановкой |
|
||||
|
||||
`WhileSubscribed(5000)` - стандартный выбор для Android: при повороте экрана подписчик пересоздаётся за миллисекунды, и таймаут не даёт зря перезапускать запрос.
|
||||
|
||||
## Как выбрать
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Нужен поток значений"] --> B{"Один подписчик,<br/>работа по запросу?"}
|
||||
B -->|"да"| C["обычный Flow<br/>холодный"]
|
||||
B -->|"нет, нужно делить"| D{"Есть текущее<br/>значение?"}
|
||||
D -->|"да, это состояние"| E["StateFlow<br/>initialValue обязателен"]
|
||||
D -->|"нет, это события"| F{"Нужна история<br/>для новых подписчиков?"}
|
||||
F -->|"нет"| G["SharedFlow<br/>replay = 0"]
|
||||
F -->|"да, n последних"| H["SharedFlow<br/>replay = n"]
|
||||
```
|
||||
|
||||
| | `StateFlow` | `SharedFlow` |
|
||||
|---|---|---|
|
||||
| начальное значение | обязательно | нет |
|
||||
| `.value` | есть | нет |
|
||||
| дедупликация | да, по `equals` | нет |
|
||||
| `replay` | всегда 1 | настраивается |
|
||||
| потеря промежуточных | да, конфляция | зависит от буфера |
|
||||
| типичное применение | состояние экрана, настройки | навигация, снекбары, ошибки |
|
||||
|
||||
Практическое правило: состояние (что сейчас на экране) - `StateFlow`; события (что произошло один раз) - `SharedFlow`. Показ сообщения об ошибке через `StateFlow` приводит к повторному показу при пересоздании подписчика.
|
||||
|
||||
## Состояние экрана целиком
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
data class ScreenState(
|
||||
val loading: Boolean = false,
|
||||
val items: List<String> = emptyList(),
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
class ViewModel(private val scope: CoroutineScope) {
|
||||
private val _state = MutableStateFlow(ScreenState())
|
||||
val state: StateFlow<ScreenState> = _state.asStateFlow()
|
||||
|
||||
fun load() {
|
||||
scope.launch {
|
||||
_state.update { it.copy(loading = true, error = null) }
|
||||
delay(20)
|
||||
_state.update { it.copy(loading = false, items = listOf("первый", "второй")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val vm = ViewModel(this)
|
||||
val job = launch { vm.state.collect { println(it) } }
|
||||
delay(10)
|
||||
vm.load()
|
||||
delay(60)
|
||||
job.cancel()
|
||||
// ScreenState(loading=false, items=[], error=null)
|
||||
// ScreenState(loading=true, items=[], error=null)
|
||||
// ScreenState(loading=false, items=[первый, второй], error=null)
|
||||
}
|
||||
```
|
||||
Один `StateFlow` на всё состояние лучше трёх отдельных: подписчик видит согласованный снимок, а не промежуточные комбинации полей. `copy` + `update` делают изменение атомарным. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
|
||||
|
||||
## Производные состояния
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val query = MutableStateFlow("")
|
||||
val items = MutableStateFlow(listOf("кот", "кофе", "книга"))
|
||||
|
||||
val filtered = combine(query, items) { q, list ->
|
||||
list.filter { it.startsWith(q) }
|
||||
}.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
delay(20) // Eagerly запускает корутину, но не мгновенно
|
||||
println(filtered.value) // [кот, кофе, книга]
|
||||
query.value = "ко"
|
||||
delay(20)
|
||||
println(filtered.value) // [кот, кофе]
|
||||
|
||||
coroutineContext.cancelChildren()
|
||||
}
|
||||
```
|
||||
`combine` + `stateIn` - способ получить вычисляемое состояние из нескольких источников. Важно вызвать `cancelChildren()` или ограничить scope: `stateIn` с `Eagerly` запускает корутину, которая живёт до отмены области.
|
||||
|
||||
## subscriptionCount
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val events = MutableSharedFlow<Int>()
|
||||
println("подписчиков: ${events.subscriptionCount.value}") // подписчиков: 0
|
||||
|
||||
val job = launch { events.collect { } }
|
||||
delay(20)
|
||||
println("после подписки: ${events.subscriptionCount.value}") // после подписки: 1
|
||||
|
||||
job.cancelAndJoin()
|
||||
println("после отмены: ${events.subscriptionCount.value}") // после отмены: 0
|
||||
}
|
||||
```
|
||||
Это `StateFlow<Int>`, поэтому за числом подписчиков можно наблюдать - именно так `WhileSubscribed` понимает, когда останавливать источник.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `Observable` + `Observer` (паттерн) | `StateFlow` |
|
||||
> | `PropertyChangeListener` | `StateFlow` |
|
||||
> | RxJava `BehaviorSubject` | `StateFlow` |
|
||||
> | RxJava `PublishSubject` | `SharedFlow(replay = 0)` |
|
||||
> | RxJava `ReplaySubject(n)` | `SharedFlow(replay = n)` |
|
||||
> | `AtomicReference` + слушатели | `MutableStateFlow` |
|
||||
> | `CopyOnWriteArrayList<Listener>` | встроенная многоадресность |
|
||||
>
|
||||
> **Паттерн «наблюдатель» вручную.** В Java его пишут заново в каждом проекте: список слушателей, синхронизация при добавлении и удалении, защита от `ConcurrentModificationException` при оповещении, снятие слушателей при закрытии компонента. Классическая реализация:
|
||||
> ```java
|
||||
> private final List<Consumer<Integer>> listeners = new CopyOnWriteArrayList<>();
|
||||
> private volatile int value;
|
||||
>
|
||||
> public void setValue(int v) {
|
||||
> this.value = v;
|
||||
> for (var l : listeners) l.accept(v);
|
||||
> }
|
||||
> public void addListener(Consumer<Integer> l) { listeners.add(l); }
|
||||
> public void removeListener(Consumer<Integer> l) { listeners.remove(l); }
|
||||
> ```
|
||||
> Здесь уже есть проблемы: оповещение идёт в потоке вызывающего, медленный слушатель задерживает остальных, забытый `removeListener` - утечка. `MutableStateFlow` решает всё это за одну строку, а отписка происходит автоматически при отмене корутины-подписчика.
|
||||
>
|
||||
> **`BehaviorSubject` против `StateFlow`.** Прямой аналог, но с двумя отличиями. `BehaviorSubject` не дедуплицирует одинаковые значения - в Rx для этого добавляют `distinctUntilChanged()` руками. И `Subject` можно «завершить» через `onComplete`/`onError`, после чего он мёртв; `StateFlow` не завершается никогда - ошибки передают внутри состояния (поле `error`), что для UI удобнее: экран всегда в каком-то состоянии.
|
||||
>
|
||||
> **Утечки подписок.** В Rx подписка возвращает `Disposable`, который нужно хранить и вызывать `dispose()`; для борьбы с забывчивостью придумали `CompositeDisposable` и `AutoDispose`. В Kotlin подписчик - корутина, поэтому отмена scope отписывает всех:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val state = MutableStateFlow(0)
|
||||
> val scope = CoroutineScope(Job())
|
||||
> scope.launch { state.collect { } }
|
||||
> delay(20)
|
||||
> println("подписчиков: ${state.subscriptionCount.value}")
|
||||
> scope.cancel()
|
||||
> delay(20)
|
||||
> println("после cancel: ${state.subscriptionCount.value}")
|
||||
> // подписчиков: 1
|
||||
> // после cancel: 0
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`volatile` поле против `StateFlow`.** `volatile int value` даёт видимость между потоками, но не даёт ни оповещения, ни атомарного обновления (`value++` всё равно гонка - нужен `AtomicInteger`). `MutableStateFlow` совмещает три роли: потокобезопасное хранилище, атомарный `update` и многоадресная рассылка.
|
||||
>
|
||||
> **Нет аналога `WhileSubscribed`.** В Java управление жизненным циклом источника при появлении и исчезновении слушателей пишут вручную: счётчик подписчиков, запуск потока при первом, остановка при последнем, таймаут на повторное подключение. `shareIn(started = WhileSubscribed(5000))` - это одна строка.
|
||||
>
|
||||
> Сравнить: [Многопоточность в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [Паттерны проектирования](../Функциональный%20Kotlin/Паттерны%20проектирования.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`StateFlow` не доставит одинаковое значение дважды:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val showError = MutableStateFlow("ошибка сети")
|
||||
> var shown = 0
|
||||
> val job = launch { showError.collect { shown++ } }
|
||||
> delay(20)
|
||||
> showError.value = "ошибка сети" // то же самое — не придёт
|
||||
> delay(20)
|
||||
> job.cancel()
|
||||
> println("показов: $shown") // показов: 1
|
||||
> }
|
||||
> ```
|
||||
> Для повторяющихся событий (две одинаковые ошибки подряд) нужен `SharedFlow`.
|
||||
>
|
||||
> **Изменяемое состояние внутри `StateFlow` ломает оповещение.** Если вы положили `MutableList` и меняете его содержимое, `equals` вернёт `true` и подписчики ничего не узнают. Храните неизменяемые `data class` и обновляйте через `copy`.
|
||||
>
|
||||
> **`state.value = state.value + 1` - гонка:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val state = MutableStateFlow(0)
|
||||
> withContext(Dispatchers.Default) {
|
||||
> List(50) { launch { repeat(200) { state.value = state.value + 1 } } }
|
||||
> .forEach { it.join() }
|
||||
> }
|
||||
> println("обновления потеряны: ${state.value < 10_000}") // обновления потеряны: true
|
||||
> }
|
||||
> ```
|
||||
> Используйте `update { }`.
|
||||
>
|
||||
> **`SharedFlow` без подписчиков теряет события.** `emit` при `replay = 0` и нулевом буфере просто завершится, не доставив ничего. Для «событий, которые нельзя потерять» нужен `replay ≥ 1` или `Channel`. Подробнее: [Channels](Channels.md)
|
||||
>
|
||||
> **`emit` в `SharedFlow` с `SUSPEND` приостанавливается на медленном подписчике:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
> import kotlin.system.measureTimeMillis
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val events = MutableSharedFlow<Int>()
|
||||
> val job = launch { events.collect { delay(100) } }
|
||||
> delay(20)
|
||||
> val t = measureTimeMillis { events.emit(1); events.emit(2) }
|
||||
> println("emit ждал подписчика: ${t >= 90}") // emit ждал подписчика: true
|
||||
> job.cancel()
|
||||
> }
|
||||
> ```
|
||||
> Один зависший подписчик блокирует отправку всем. Задавайте `extraBufferCapacity` и стратегию переполнения.
|
||||
>
|
||||
> **`tryEmit` возвращает `true` без подписчиков**, хотя событие никто не получил. Проверка `if (tryEmit(x))` не означает «доставлено».
|
||||
>
|
||||
> **`DROP_OLDEST` без буфера не работает.** При `replay = 0` и `extraBufferCapacity = 0` стратегия переполнения игнорируется, потому что буфера нет - `emit` будет приостанавливаться.
|
||||
>
|
||||
> **`SharingStarted.Eagerly` держит источник навсегда:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> var active = 0
|
||||
> val source = flow {
|
||||
> active++
|
||||
> try { repeat(1000) { delay(10); emit(it) } } finally { active-- }
|
||||
> }
|
||||
> val hot = source.stateIn(this, SharingStarted.Eagerly, -1)
|
||||
> delay(30)
|
||||
> println("работает без единого подписчика: ${active == 1}")
|
||||
> coroutineContext.cancelChildren()
|
||||
> // работает без единого подписчика: true
|
||||
> }
|
||||
> ```
|
||||
> Если источник - опрос сервера, он будет работать и жечь батарею, пока жив scope. Для подписок UI нужен `WhileSubscribed()`.
|
||||
>
|
||||
> **`stateIn`/`shareIn` требуют scope, который кто-то отменит.** Передача `GlobalScope` или scope, живущего весь процесс, превращает их в утечку.
|
||||
>
|
||||
> **`StateFlow.value` - снимок, а не транзакция:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val a = MutableStateFlow(1)
|
||||
> val b = MutableStateFlow(10)
|
||||
> // между чтениями значения могли измениться
|
||||
> val sum = a.value + b.value
|
||||
> println("несогласованный снимок возможен: $sum") // несогласованный снимок возможен: 11
|
||||
> }
|
||||
> ```
|
||||
> Два связанных значения нужно держать в одном `StateFlow` как единый объект состояния.
|
||||
>
|
||||
> **`collect` на `StateFlow` никогда не завершается.** Это бесконечный поток: `toList()` на нём повиснет, `onCompletion` не вызовется сам. Нужен `take(n)`, `first()` или отмена корутины.
|
||||
>
|
||||
> **Подписчик, созданный до `emit`, может его пропустить:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.flow.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val events = MutableSharedFlow<Int>()
|
||||
> launch { events.collect { println("не увидит: $it") } }
|
||||
> events.emit(1) // launch ещё не успел подписаться
|
||||
> delay(20)
|
||||
> coroutineContext.cancelChildren()
|
||||
> // (ничего не напечатано)
|
||||
> }
|
||||
> ```
|
||||
> `launch` возвращается до начала тела, поэтому подписка ещё не оформлена. В тестах для этого используют `events.subscriptionCount.first { it > 0 }`, в продакшене - `replay ≥ 1`.
|
||||
>
|
||||
> **`asStateFlow()` не копирует состояние** - это обёртка только для чтения над тем же объектом. Если внешний код получит доступ к исходному `MutableStateFlow`, он сможет его менять.
|
||||
>
|
||||
> **Ошибка в подписчике отменяет его подписку, а не поток.** Исключение в теле `collect` уходит в корутину-подписчик и может уронить её scope, при этом `StateFlow` продолжит жить. Оборачивайте тело `collect` в `try/catch`, если ошибки ожидаемы.
|
||||
|
||||
Далее: [Channels](Channels.md)
|
||||
@@ -0,0 +1,395 @@
|
||||
---
|
||||
title: Suspend-функции
|
||||
description: Модификатор suspend, Continuation, машина состояний и мост к колбэчным API
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
`suspend` означает: эта функция может приостановиться, поэтому вызывать её разрешено только оттуда, где приостановка возможна.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun fetchData(): String {
|
||||
delay(100) // точка приостановки
|
||||
return "данные"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(fetchData()) // данные
|
||||
}
|
||||
```
|
||||
Вне корутины вызов не скомпилируется: `fun main() { fetchData() }` даёт «Suspend function can only be called within coroutine body».
|
||||
|
||||
## Что добавляет компилятор
|
||||
`suspend fun fetchData(): String` превращается в метод с дополнительным параметром:
|
||||
```kotlin
|
||||
// примерно так это выглядит с точки зрения Java:
|
||||
// Object fetchData(Continuation<? super String> continuation)
|
||||
```
|
||||
`Continuation` - это «что делать дальше»: объект с методом `resumeWith`, хранящий сохранённое состояние функции.
|
||||
```kotlin
|
||||
import kotlin.coroutines.*
|
||||
|
||||
fun main() {
|
||||
val continuation = object : Continuation<String> {
|
||||
override val context = EmptyCoroutineContext
|
||||
override fun resumeWith(result: Result<String>) {
|
||||
println("получено: ${result.getOrNull()}")
|
||||
}
|
||||
}
|
||||
continuation.resumeWith(Result.success("значение")) // получено: значение
|
||||
}
|
||||
```
|
||||
Отсюда и название CPS - continuation-passing style. Компилятор делает это преобразование автоматически, поэтому в исходнике остаётся обычный последовательный код.
|
||||
|
||||
## Машина состояний
|
||||
Каждая точка приостановки превращается в метку состояния:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun threeSteps(): Int {
|
||||
println("шаг 1") // состояние 0
|
||||
delay(10)
|
||||
println("шаг 2") // состояние 1
|
||||
delay(10)
|
||||
println("шаг 3") // состояние 2
|
||||
return 42
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(threeSteps())
|
||||
// шаг 1
|
||||
// шаг 2
|
||||
// шаг 3
|
||||
// 42
|
||||
}
|
||||
```
|
||||
В байт-коде это один метод с `switch` по полю `label`: при каждом возобновлении он входит в себя заново и прыгает к нужной метке. Локальные переменные, живущие между приостановками, становятся полями объекта-продолжения.
|
||||
|
||||
Практические следствия:
|
||||
- приостановка стоит дешевле переключения потока, но дороже обычного вызова
|
||||
- глубоко вложенные `suspend`-вызовы создают по объекту продолжения на кадр
|
||||
- стектрейс восстанавливается библиотекой, а не JVM - поэтому он может быть короче ожидаемого
|
||||
|
||||
## Функция, которая не приостанавливается
|
||||
`suspend` не обязывает приостанавливаться:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun maybeSlow(cached: String?): String {
|
||||
cached?.let { return it } // быстрый путь: без приостановки
|
||||
delay(100)
|
||||
return "загружено"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(maybeSlow("из кеша")) // из кеша
|
||||
println(maybeSlow(null)) // загружено
|
||||
}
|
||||
```
|
||||
Если приостановки не произошло, функция возвращает значение напрямую - без накладных расходов. Это важно для «горячих» путей: `suspend` не означает «всегда медленно».
|
||||
|
||||
Компилятор предупреждает о `suspend` без точек приостановки:
|
||||
```kotlin
|
||||
suspend fun noSuspension(): Int = 42 // warning: redundant 'suspend' modifier
|
||||
|
||||
fun main() {
|
||||
println("нужен вызов из корутины или runBlocking")
|
||||
}
|
||||
```
|
||||
Такой модификатор стоит убрать - он заставляет вызывающих быть `suspend` без причины.
|
||||
|
||||
## suspend-лямбды и типы
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun retry(times: Int, block: suspend () -> String): String {
|
||||
repeat(times - 1) { attempt ->
|
||||
try {
|
||||
return block()
|
||||
} catch (e: Exception) {
|
||||
println("попытка ${attempt + 1}: ${e.message}")
|
||||
delay(10)
|
||||
}
|
||||
}
|
||||
return block()
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
var counter = 0
|
||||
val result = retry(3) {
|
||||
counter++
|
||||
delay(5)
|
||||
if (counter < 3) error("не готово") else "успех"
|
||||
}
|
||||
println(result)
|
||||
// попытка 1: не готово
|
||||
// попытка 2: не готово
|
||||
// успех
|
||||
}
|
||||
```
|
||||
Тип `suspend () -> String` - функциональный тип, внутри которого можно приостанавливаться. Без `suspend` в типе параметра `delay` внутри лямбды не скомпилируется.
|
||||
|
||||
Для лямбды с получателем аналогично: `suspend CoroutineScope.() -> Unit` - именно так объявлены `launch` и `runBlocking`.
|
||||
|
||||
## Приостановка без блокировки: suspendCancellableCoroutine
|
||||
Это мост от колбэчного API к `suspend`-функции - основной способ интегрировать существующие библиотеки.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
// колбэчный API, который нужно обернуть
|
||||
fun legacyLoad(onSuccess: (String) -> Unit, onError: (Throwable) -> Unit) {
|
||||
Thread {
|
||||
Thread.sleep(50)
|
||||
onSuccess("результат из колбэка")
|
||||
}.start()
|
||||
}
|
||||
|
||||
suspend fun load(): String = suspendCancellableCoroutine { cont ->
|
||||
legacyLoad(
|
||||
onSuccess = { cont.resume(it) },
|
||||
onError = { cont.resumeWithException(it) },
|
||||
)
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(load()) // результат из колбэка
|
||||
}
|
||||
```
|
||||
Правила: `resume` или `resumeWithException` нужно вызвать **ровно один раз**. Повторный вызов бросит `IllegalStateException`.
|
||||
|
||||
Отмену нужно поддержать явно:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
suspend fun awaitFuture(future: CompletableFuture<String>): String =
|
||||
suspendCancellableCoroutine { cont ->
|
||||
future.whenComplete { value, error ->
|
||||
if (error != null) cont.cancel(error) else cont.resume(value)
|
||||
}
|
||||
cont.invokeOnCancellation { future.cancel(true) } // освобождаем ресурс
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val future = CompletableFuture.supplyAsync {
|
||||
Thread.sleep(30)
|
||||
"из future"
|
||||
}
|
||||
println(awaitFuture(future)) // из future
|
||||
}
|
||||
```
|
||||
`invokeOnCancellation` - то, что отличает корректную обёртку от утечки: при отмене корутины нижележащий запрос тоже нужно отменить. Подробнее: [Отмена и таймауты](Отмена%20и%20таймауты.md)
|
||||
|
||||
## Последовательность и параллельность
|
||||
`suspend`-функции по умолчанию выполняются последовательно:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
suspend fun step(name: String): String { delay(100); return name }
|
||||
|
||||
fun main() = runBlocking {
|
||||
val sequential = measureTimeMillis {
|
||||
val a = step("a")
|
||||
val b = step("b")
|
||||
println("$a$b") // ab
|
||||
}
|
||||
println("последовательно около 200 мс: ${sequential >= 200}") // последовательно около 200 мс: true
|
||||
|
||||
val parallel = measureTimeMillis {
|
||||
coroutineScope {
|
||||
val a = async { step("a") }
|
||||
val b = async { step("b") }
|
||||
println("${a.await()}${b.await()}") // ab
|
||||
}
|
||||
}
|
||||
println("параллельно около 100 мс: ${parallel < 180}") // параллельно около 100 мс: true
|
||||
}
|
||||
```
|
||||
Это сознательное решение: последовательность - разумное поведение по умолчанию, параллельность требуется явно запросить через `async`. Подробнее: [Билдеры корутин](Билдеры%20корутин.md)
|
||||
|
||||
## Правила проектирования suspend-API
|
||||
| Правило | Почему |
|
||||
|---|---|
|
||||
| `suspend fun` не должна блокировать поток | иначе теряется весь смысл |
|
||||
| смена диспетчера - внутри функции, а не у вызывающего | вызывающий не должен знать, I/O это или CPU |
|
||||
| функция должна быть безопасна для отмены | иначе отмена не работает или течёт |
|
||||
| возвращать значение, а не `Deferred` | параллельность - дело вызывающего |
|
||||
| `suspend` + возврат `Flow` обычно излишни | `Flow` сам по себе холодный |
|
||||
|
||||
Правило про диспетчер - самое практичное:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
// хорошо: вызывающему не нужно думать о потоках
|
||||
suspend fun readConfig(): String = withContext(Dispatchers.IO) {
|
||||
"содержимое файла" // здесь был бы блокирующий File.readText
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(readConfig()) // содержимое файла
|
||||
}
|
||||
```
|
||||
Такая функция называется main-safe: её можно вызвать с любого потока, включая UI.
|
||||
|
||||
## suspend и наследование
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
interface Repository {
|
||||
suspend fun find(id: Int): String?
|
||||
}
|
||||
|
||||
class RemoteRepository : Repository {
|
||||
override suspend fun find(id: Int): String? {
|
||||
delay(10)
|
||||
return if (id == 1) "найдено" else null
|
||||
}
|
||||
}
|
||||
|
||||
class CachedRepository : Repository {
|
||||
private val cache = mapOf(1 to "из кеша")
|
||||
override suspend fun find(id: Int): String? = cache[id] // без приостановки
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(RemoteRepository().find(1)) // найдено
|
||||
println(CachedRepository().find(1)) // из кеша
|
||||
println(CachedRepository().find(2)) // null
|
||||
}
|
||||
```
|
||||
`suspend` в интерфейсе - правильный способ объявить операцию, которая **может** быть асинхронной. Реализация решает, приостанавливаться ли. Обратное невозможно: не-suspend метод интерфейса нельзя переопределить как `suspend`.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `CompletableFuture<T> f()` | `suspend fun f(): T` |
|
||||
> | `void f(Callback<T> cb)` | `suspend fun f(): T` |
|
||||
> | `f().thenApply(x -> ...)` | `val x = f()` |
|
||||
> | `CompletionStage` в сигнатуре | `suspend` в модификаторе |
|
||||
> | блокирующий `f()` + пул потоков | `suspend` + `withContext(IO)` |
|
||||
> | нет аналога | `Continuation` |
|
||||
>
|
||||
> **Асинхронность в типе возврата против модификатора.** В Java асинхронная функция возвращает обёртку: `CompletableFuture<User>`. Это заражает тип: `List<CompletableFuture<User>>` приходится разворачивать через `allOf`, а вложенные вызовы дают `CompletableFuture<CompletableFuture<T>>` и `thenCompose` вместо `thenApply`. В Kotlin тип возврата остаётся обычным (`User`), а асинхронность выражается модификатором:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> suspend fun loadOne(id: Int): String { delay(10); return "u$id" }
|
||||
>
|
||||
> suspend fun loadMany(ids: List<Int>): List<String> = coroutineScope {
|
||||
> ids.map { async { loadOne(it) } }.awaitAll()
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking { println(loadMany(listOf(1, 2, 3))) } // [u1, u2, u3]
|
||||
> ```
|
||||
> В Java это `CompletableFuture.allOf(...)` с ручной сборкой результатов и приведением типов.
|
||||
>
|
||||
> **`Continuation` - это обобщённый коллбэк.** В сущности `suspend fun f(): T` компилируется в `f(Continuation<T>)`, то есть в тот же колбэчный стиль, который в Java пишут руками. Разница в том, что преобразование делает компилятор, поэтому исходник остаётся линейным, а в Java программист пишет лямбды сам и получает «callback hell» при вложенности.
|
||||
>
|
||||
> **Из Java `suspend`-функцию вызвать неудобно.** Она видна как метод с лишним параметром `Continuation`, возвращающий `Object`. Для интеграции есть готовые мосты: `future { }` из `kotlinx-coroutines-jdk8` возвращает `CompletableFuture`, а `runBlocking` даёт синхронный вызов. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||||
>
|
||||
> **Виртуальные потоки убирают необходимость в модификаторе.** В Java 21 блокирующий `loadOne(id)` на виртуальном потоке масштабируется без изменения сигнатуры - это главный плюс Loom: существующий код работает как есть. Цена - компилятор не знает, где функция блокируется, поэтому нет ни структурированной отмены по дереву, ни проверки «не блокируй UI-поток» на этапе компиляции.
|
||||
>
|
||||
> Сравнить: [Многопоточность в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Блокировка внутри `suspend` сводит корутины на нет:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> // плохо: функция объявлена suspend, но блокирует поток
|
||||
> suspend fun badRead(): String {
|
||||
> Thread.sleep(50)
|
||||
> return "заблокировал поток"
|
||||
> }
|
||||
>
|
||||
> // хорошо: блокирующий код в подходящем диспетчере
|
||||
> suspend fun goodRead(): String = withContext(Dispatchers.IO) {
|
||||
> Thread.sleep(50)
|
||||
> "поток из IO-пула"
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> println(badRead())
|
||||
> println(goodRead())
|
||||
> // заблокировал поток
|
||||
> // поток из IO-пула
|
||||
> }
|
||||
> ```
|
||||
> Компилятор это не поймает: `suspend` не запрещает блокировку. Ошибка проявится как «приложение зависает под нагрузкой».
|
||||
>
|
||||
> **Повторный `resume` бросает исключение:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlin.coroutines.resume
|
||||
>
|
||||
> suspend fun broken(): String = suspendCancellableCoroutine { cont ->
|
||||
> cont.resume("первый")
|
||||
> // cont.resume("второй") // IllegalStateException: Already resumed
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking { println(broken()) } // первый
|
||||
> ```
|
||||
> Колбэчные API, которые могут вызвать коллбэк дважды (например, при ретраях внутри), требуют защиты флагом или `isActive`-проверки.
|
||||
>
|
||||
> **Обёртка без `invokeOnCancellation` течёт.** При отмене корутины нижележащий запрос продолжит работу: соединение останется открытым, таймер - запущенным, файл - читаемым. Всегда освобождайте ресурс в `invokeOnCancellation`.
|
||||
>
|
||||
> **`suspendCoroutine` без `Cancellable` игнорирует отмену:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch {
|
||||
> try {
|
||||
> withTimeout(30) { delay(1000) }
|
||||
> } catch (e: TimeoutCancellationException) {
|
||||
> println("таймаут сработал: ${e::class.simpleName}")
|
||||
> }
|
||||
> }
|
||||
> job.join()
|
||||
> }
|
||||
> ```
|
||||
> `delay` отменяем, поэтому таймаут работает. А вот `suspendCoroutine { }` (без `Cancellable`) приостановится навсегда и таймаут его не прервёт - используйте `suspendCancellableCoroutine`.
|
||||
>
|
||||
> **`suspend` поднимается по всему стеку вызовов.** Добавить приостановку в глубоко вложенную функцию означает помечать `suspend` всю цепочку вверх. Если наверху - не-suspend интерфейс (например, `Comparator` или `equals`), сделать это невозможно, и придётся либо менять архитектуру, либо блокировать через `runBlocking`.
|
||||
>
|
||||
> **`suspend` нельзя использовать в конструкторе, геттере свойства и `init`:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> class Service {
|
||||
> // suspend init — невозможно
|
||||
> // val value: String get() = loadValue() // геттер не может быть suspend
|
||||
>
|
||||
> suspend fun loadValue(): String { delay(10); return "значение" }
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking { println(Service().loadValue()) } // значение
|
||||
> ```
|
||||
> Для асинхронной инициализации используют отдельную `suspend fun init()` или `lazy`-обёртку через `Deferred`.
|
||||
>
|
||||
> **Нельзя переопределить обычный метод как `suspend`.** Если интерфейс объявлен в Java (`Runnable`, `Callable`, `Comparator`), асинхронность туда не поместится - только `runBlocking` внутри, что блокирует поток.
|
||||
>
|
||||
> **Стектрейсы обрезаны.** Исключение из корутины содержит кадры только текущей точки возобновления, а не всю логическую цепочку. Для отладки включают `-Dkotlinx.coroutines.debug` или добавляют `kotlinx-coroutines-debug`, которые восстанавливают историю приостановок.
|
||||
>
|
||||
> **Рекурсивная `suspend`-функция создаёт объект продолжения на каждый кадр:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> suspend fun countDown(n: Int): Int = if (n <= 0) 0 else countDown(n - 1) + 1
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> println(countDown(1000)) // 1000
|
||||
> }
|
||||
> ```
|
||||
> Глубина ограничена не стеком JVM, а памятью, но на десятках тысяч кадров это заметная аллокация. Хвостовая рекурсия (`tailrec`) с `suspend` не сочетается.
|
||||
>
|
||||
> **`suspend` без приостановок - лишний.** Он заставляет всех вызывающих быть `suspend`, не давая ничего взамен. Компилятор предупреждает; предупреждение стоит слушать.
|
||||
|
||||
Далее: [Структурированная конкурентность](Структурированная%20конкурентность.md)
|
||||
@@ -0,0 +1,417 @@
|
||||
---
|
||||
title: Билдеры корутин
|
||||
description: launch, async, runBlocking, withContext, produce и actor - что выбрать и чем они различаются
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
Билдер - функция, которая создаёт корутину. Их немного, и выбор между ними определяется двумя вопросами: нужен ли результат и нужна ли новая корутина вообще.
|
||||
|
||||
| Билдер | Возвращает | Блокирует поток | Нужен результат |
|
||||
|---|---|---|---|
|
||||
| `launch` | `Job` | нет | нет |
|
||||
| `async` | `Deferred<T>` | нет | да |
|
||||
| `runBlocking` | `T` | да | да |
|
||||
| `withContext` | `T` | нет | да, смена контекста |
|
||||
| `produce` | `ReceiveChannel<T>` | нет | поток значений |
|
||||
| `actor` | `SendChannel<T>` | нет | обработка сообщений |
|
||||
|
||||
## launch: запустить и не ждать результата
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch {
|
||||
delay(50)
|
||||
println("работа выполнена")
|
||||
}
|
||||
println("launch вернулся сразу: ${job.isActive}")
|
||||
job.join()
|
||||
println("дождались")
|
||||
// launch вернулся сразу: true
|
||||
// работа выполнена
|
||||
// дождались
|
||||
}
|
||||
```
|
||||
`launch` возвращает `Job` - дескриптор для `join()`, `cancel()` и проверки состояния. Результат тела отбрасывается: тип всегда `Job`, даже если блок что-то вычислил.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch { 42 } // значение 42 теряется
|
||||
job.join()
|
||||
println(job::class.simpleName != null) // true
|
||||
}
|
||||
```
|
||||
|
||||
## async: запустить и получить результат
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
suspend fun loadPrice(id: Int): Int { delay(100); return id * 10 }
|
||||
|
||||
fun main() = runBlocking {
|
||||
val time = measureTimeMillis {
|
||||
val a = async { loadPrice(1) }
|
||||
val b = async { loadPrice(2) }
|
||||
println("сумма: ${a.await() + b.await()}") // сумма: 30
|
||||
}
|
||||
println("параллельно: ${time < 180}") // параллельно: true
|
||||
}
|
||||
```
|
||||
`Deferred<T>` - это `Job` с результатом: у него есть и `join()`, и `await()`. Вызов `await()` приостанавливает корутину до готовности значения.
|
||||
|
||||
`await` можно вызывать повторно - результат кешируется:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
var calls = 0
|
||||
val d = async { calls++; delay(20); "значение" }
|
||||
println(d.await()) // значение
|
||||
println(d.await()) // значение
|
||||
println("тело выполнилось раз: $calls") // тело выполнилось раз: 1
|
||||
}
|
||||
```
|
||||
|
||||
Ленивый запуск - тело не начнётся до `await()` или `start()`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val lazy = async(start = CoroutineStart.LAZY) {
|
||||
println("тело стартовало")
|
||||
"результат"
|
||||
}
|
||||
println("создан, но не запущен")
|
||||
delay(20)
|
||||
println(lazy.await())
|
||||
// создан, но не запущен
|
||||
// тело стартовало
|
||||
// результат
|
||||
}
|
||||
```
|
||||
Внимание: ленивый `async`, у которого так и не вызвали `await()`, никогда не выполнится и заблокирует завершение области до её отмены.
|
||||
|
||||
## runBlocking: мост из синхронного кода
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun compute(): Int { delay(20); return 42 }
|
||||
|
||||
fun main() {
|
||||
val result = runBlocking { compute() } // обычная функция вызывает suspend
|
||||
println(result) // 42
|
||||
}
|
||||
```
|
||||
`runBlocking` блокирует текущий поток до завершения корутины и всех её детей. Это единственный билдер, который **не** требует scope - он его создаёт.
|
||||
|
||||
Применимость:
|
||||
| Место | Уместен ли `runBlocking` |
|
||||
|---|---|
|
||||
| функция `main` | да |
|
||||
| тесты (если нет `runTest`) | да |
|
||||
| CLI-утилита | да |
|
||||
| обработчик HTTP-запроса | нет, фреймворк даёт scope |
|
||||
| Android UI-поток | категорически нет |
|
||||
| внутри другой корутины | нет, это блокировка потока |
|
||||
|
||||
## withContext: сменить контекст без новой корутины
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun readFile(): String = withContext(Dispatchers.IO) {
|
||||
Thread.sleep(20) // здесь блокирующий вызов допустим
|
||||
"содержимое"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(readFile()) // содержимое
|
||||
}
|
||||
```
|
||||
`withContext` не создаёт новую корутину - он переключает контекст текущей и возвращает значение блока. Это главный инструмент для «main-safe» функций.
|
||||
|
||||
Разница с `async { }.await()` существенна:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun viaWithContext(): Int = withContext(Dispatchers.Default) { 1 }
|
||||
|
||||
suspend fun viaAsync(): Int = coroutineScope {
|
||||
async(Dispatchers.Default) { 1 }.await() // лишняя корутина
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println("${viaWithContext()} ${viaAsync()}") // 1 1
|
||||
}
|
||||
```
|
||||
Результат одинаков, но `async().await()` создаёт объект `Deferred` и корутину зря. Правило: если результат нужен **сразу**, это `withContext`; `async` оправдан только когда между запуском и `await` есть другая работа.
|
||||
|
||||
## Параллельная декомпозиция
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
data class Dashboard(val user: String, val orders: List<String>, val balance: Int)
|
||||
|
||||
suspend fun fetchUser(): String { delay(30); return "Иван" }
|
||||
suspend fun fetchOrders(): List<String> { delay(40); return listOf("заказ 1") }
|
||||
suspend fun fetchBalance(): Int { delay(20); return 1000 }
|
||||
|
||||
suspend fun loadDashboard(): Dashboard = coroutineScope {
|
||||
val user = async { fetchUser() }
|
||||
val orders = async { fetchOrders() }
|
||||
val balance = async { fetchBalance() }
|
||||
Dashboard(user.await(), orders.await(), balance.await())
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(loadDashboard())
|
||||
// Dashboard(user=Иван, orders=[заказ 1], balance=1000)
|
||||
}
|
||||
```
|
||||
Это каноничный шаблон: `coroutineScope` + несколько `async` + сборка результата. Время работы - максимум из трёх, а не сумма; при ошибке любого запроса остальные отменяются.
|
||||
|
||||
## CoroutineStart
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
launch(start = CoroutineStart.DEFAULT) { println("3: обычный, после текущего кода") }
|
||||
launch(start = CoroutineStart.UNDISPATCHED) { println("1: сразу, до первой приостановки") }
|
||||
val lazy = launch(start = CoroutineStart.LAZY) { println("4: только после start()") }
|
||||
println("2: тело main")
|
||||
lazy.start()
|
||||
delay(20)
|
||||
// 1: сразу, до первой приостановки
|
||||
// 2: тело main
|
||||
// 3: обычный, после текущего кода
|
||||
// 4: только после start()
|
||||
}
|
||||
```
|
||||
| Значение | Поведение |
|
||||
|---|---|
|
||||
| `DEFAULT` | запланировать запуск согласно диспетчеру |
|
||||
| `LAZY` | не запускать до `start()`, `join()` или `await()` |
|
||||
| `ATOMIC` | запустить, игнорируя отмену до первой точки приостановки |
|
||||
| `UNDISPATCHED` | выполнять в текущем потоке до первой приостановки |
|
||||
|
||||
`UNDISPATCHED` используют, когда важно выполнить начало тела немедленно (например, зарегистрировать подписку до возврата из функции). Остальные значения нужны редко.
|
||||
|
||||
## produce и actor
|
||||
`produce` - билдер, возвращающий канал с результатами:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
fun CoroutineScope.numbers(): ReceiveChannel<Int> = produce {
|
||||
for (i in 1..3) {
|
||||
delay(10)
|
||||
send(i)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val channel = numbers()
|
||||
for (value in channel) print("$value ")
|
||||
println() // 1 2 3
|
||||
}
|
||||
```
|
||||
`actor` - обратное: корутина, принимающая сообщения:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
sealed interface Msg {
|
||||
data class Add(val value: Int) : Msg
|
||||
data class GetSum(val response: CompletableDeferred<Int>) : Msg
|
||||
}
|
||||
|
||||
fun CoroutineScope.counter() = actor<Msg> {
|
||||
var sum = 0
|
||||
for (msg in channel) {
|
||||
when (msg) {
|
||||
is Msg.Add -> sum += msg.value
|
||||
is Msg.GetSum -> msg.response.complete(sum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val c = counter()
|
||||
c.send(Msg.Add(10))
|
||||
c.send(Msg.Add(5))
|
||||
val response = CompletableDeferred<Int>()
|
||||
c.send(Msg.GetSum(response))
|
||||
println(response.await()) // 15
|
||||
c.close()
|
||||
}
|
||||
```
|
||||
Оба помечены `@ObsoleteCoroutinesApi` - их планируют заменить. Для потоков значений используйте [Flow](Flow.md), для разделяемого состояния - [Mutex или StateFlow](Разделяемое%20состояние.md).
|
||||
|
||||
## Как выбирать
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Нужна корутина"] --> B{"Вы в suspend-функции<br/>или в scope?"}
|
||||
B -->|"нет, обычный код"| C["runBlocking"]
|
||||
B -->|"да"| D{"Нужен результат?"}
|
||||
D -->|"нет, fire-and-forget"| E["launch"]
|
||||
D -->|"да"| F{"Нужен прямо сейчас?"}
|
||||
F -->|"да, просто другой поток"| G["withContext"]
|
||||
F -->|"нет, сначала другая работа"| H["async + await"]
|
||||
```
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `executor.execute(task)` | `launch` |
|
||||
> | `executor.submit(task)` -> `Future<T>` | `async` -> `Deferred<T>` |
|
||||
> | `future.get()` (блокирует) | `deferred.await()` (приостанавливает) |
|
||||
> | `CompletableFuture.supplyAsync` | `async` |
|
||||
> | `future.get()` в `main` | `runBlocking` |
|
||||
> | `CompletableFuture.allOf` | `awaitAll` |
|
||||
> | явная передача пула в каждый вызов | контекст наследуется |
|
||||
>
|
||||
> **`await()` приостанавливает, `get()` блокирует.** Это ключевая разница. `Future.get()` занимает поток до готовности результата, поэтому в пуле из 8 потоков 8 ожидающих задач полностью его парализуют - классический deadlock при вложенных `get()`. `Deferred.await()` освобождает поток, поэтому миллион ожидающих корутин живут в нескольких потоках.
|
||||
>
|
||||
> **Контекст наследуется, а не передаётся.** В Java пул приходится протаскивать в каждый вызов: `supplyAsync(task, executor)`, и забытый второй аргумент молча отправит задачу в `ForkJoinPool.commonPool`. В Kotlin дочерняя корутина наследует контекст родителя:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> withContext(Dispatchers.IO) {
|
||||
> launch {
|
||||
> val name = Thread.currentThread().name
|
||||
> val onIo = name.contains("DefaultDispatcher")
|
||||
> println("ребёнок унаследовал IO: $onIo")
|
||||
> }
|
||||
> }
|
||||
> // ребёнок унаследовал IO: true
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`CompletableFuture` комбинируется методами, корутины - кодом.** Java предлагает `thenCombine`, `allOf`, `anyOf`, `thenCompose` - по методу на каждый случай. В Kotlin сбор результатов пишется обычным кодом (`listOf(a.await(), b.await())`), поэтому `if`, циклы и `try/catch` работают там естественно.
|
||||
>
|
||||
> **Нет аналога `runBlocking` в Java, потому что он не нужен:** `future.get()` и так блокирует. Обратная ситуация - в Kotlin `runBlocking` нужен именно как явная граница между блокирующим и приостанавливаемым миром, и эта явность полезна: видно, где поток встаёт.
|
||||
>
|
||||
> **`launch` против `submit` в обработке ошибок.** Исключение в `submit` попадает в `Future` и теряется, если не вызвать `get()`. Исключение в `launch` уходит родителю и обязательно будет обработано - либо родителем, либо `CoroutineExceptionHandler`, либо уронит приложение. Терять ошибки молча корутины не позволяют. Подробнее: [Исключения в корутинах](Исключения%20в%20корутинах.md)
|
||||
>
|
||||
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`runBlocking` внутри корутины блокирует поток диспетчера:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> withContext(Dispatchers.Default.limitedParallelism(1)) {
|
||||
> launch {
|
||||
> runBlocking { delay(50) } // держит единственный поток
|
||||
> println("первая закончила")
|
||||
> }
|
||||
> launch { println("вторая ждала освобождения потока") }
|
||||
> }
|
||||
> // первая закончила
|
||||
> // вторая ждала освобождения потока
|
||||
> }
|
||||
> ```
|
||||
> На ограниченном диспетчере это прямой путь к deadlock. Внутри корутины нужен `withContext`, а не `runBlocking`.
|
||||
>
|
||||
> **`async` без `await` проглатывает исключение до конца области:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val result = runCatching {
|
||||
> coroutineScope {
|
||||
> async { throw IllegalStateException("ошибка в async") }
|
||||
> delay(20)
|
||||
> "тело завершилось"
|
||||
> }
|
||||
> }
|
||||
> println(result.exceptionOrNull()?.message) // ошибка в async
|
||||
> }
|
||||
> ```
|
||||
> Ошибка не исчезла - она отменила область при завершении. Но заметить её в месте возникновения невозможно, а если `async` висит в `SupervisorJob`, она потеряется совсем.
|
||||
>
|
||||
> **Ленивый `async` без `await` подвешивает область навсегда:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch {
|
||||
> async(start = CoroutineStart.LAZY) { "никогда не выполнится" }
|
||||
> println("тело launch закончилось")
|
||||
> }
|
||||
> val finished = withTimeoutOrNull(200) { job.join(); true } ?: false
|
||||
> println("launch завершился: $finished")
|
||||
> job.cancel()
|
||||
> // тело launch закончилось
|
||||
> // launch завершился: false
|
||||
> }
|
||||
> ```
|
||||
> Ленивый `Deferred` остаётся незавершённым ребёнком, а родитель обязан дождаться всех детей - поэтому `launch` не завершится никогда, даже после выхода из тела. Без `job.cancel()` программа повиснет. Это тихая утечка: ни ошибки, ни предупреждения.
|
||||
>
|
||||
> **`async` последовательно - это не параллельно:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlin.system.measureTimeMillis
|
||||
>
|
||||
> suspend fun slow(): Int { delay(100); return 1 }
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val wrong = measureTimeMillis {
|
||||
> val a = async { slow() }.await() // await сразу — ждём здесь же
|
||||
> val b = async { slow() }.await()
|
||||
> println(a + b) // 2
|
||||
> }
|
||||
> println("последовательно: ${wrong >= 200}") // последовательно: true
|
||||
> }
|
||||
> ```
|
||||
> `await()` сразу после `async` убивает параллельность. Запускайте все `async`, потом ждите.
|
||||
>
|
||||
> **`launch` теряет результат молча.** Тип `Job` не содержит значения, поэтому `launch { computeSomething() }` компилируется и ничего не делает с результатом. Компилятор не предупредит.
|
||||
>
|
||||
> **`withContext` на каждый мелкий вызов стоит дорого:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> suspend fun badLoop(): Int = withContext(Dispatchers.Default) {
|
||||
> var sum = 0
|
||||
> repeat(1000) { sum += it } // одно переключение — правильно
|
||||
> sum
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> println(badLoop()) // 499500
|
||||
> }
|
||||
> ```
|
||||
> Переключение контекста - это передача задачи в другой пул с синхронизацией. Выносите `withContext` наружу цикла, а не внутрь.
|
||||
>
|
||||
> **`runBlocking` в Android UI-потоке вызывает ANR.** Это самая частая серьёзная ошибка: UI замирает до конца корутины. В Android используйте `lifecycleScope`/`viewModelScope`.
|
||||
>
|
||||
> **`produce` и `actor` устарели.** Они помечены `@ObsoleteCoroutinesApi`, их API может измениться. Для новых проектов - `Flow` и `Channel` напрямую, либо `Mutex` для состояния.
|
||||
>
|
||||
> **`CoroutineStart.ATOMIC` игнорирует отмену:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> @OptIn(ExperimentalCoroutinesApi::class)
|
||||
> val job = launch(start = CoroutineStart.ATOMIC) {
|
||||
> println("тело всё равно начнётся")
|
||||
> delay(100)
|
||||
> println("а это уже нет")
|
||||
> }
|
||||
> job.cancel()
|
||||
> job.join()
|
||||
> // тело всё равно начнётся
|
||||
> }
|
||||
> ```
|
||||
> Начало тела выполнится даже после `cancel()`. Это нужно для гарантированного освобождения ресурсов, но легко превращается в сюрприз.
|
||||
>
|
||||
> **Возврат `Deferred` из публичной функции ломает структурированность** - вызывающий может не вызвать `await()`, и ошибка потеряется. Возвращайте значение из `suspend`-функции.
|
||||
|
||||
Далее: [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md)
|
||||
@@ -0,0 +1,404 @@
|
||||
---
|
||||
title: Диспетчеры и контекст
|
||||
description: CoroutineContext как набор элементов, выбор между Default, IO и Main, limitedParallelism и наследование контекста
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
`CoroutineContext` - неизменяемый набор элементов, описывающий, как и где выполняется корутина. Диспетчер - один из этих элементов, отвечающий за поток.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.ContinuationInterceptor
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(coroutineContext[ContinuationInterceptor] != null) // true
|
||||
println(coroutineContext[Job] != null) // true
|
||||
println(coroutineContext[CoroutineName]) // null — имени не задали
|
||||
|
||||
withContext(CoroutineName("загрузка")) {
|
||||
println(coroutineContext[CoroutineName]?.name) // загрузка
|
||||
}
|
||||
}
|
||||
```
|
||||
Элементы складываются оператором `+`, а извлекаются по ключу как из `Map`. Ключ диспетчера называется `ContinuationInterceptor` - это более общее понятие «перехватчик продолжений», частным случаем которого является диспетчер. Ключ `CoroutineDispatcher` тоже существует, но помечен `@ExperimentalStdlibApi`.
|
||||
|
||||
## Из чего состоит контекст
|
||||
| Элемент | Зачем |
|
||||
|---|---|
|
||||
| `Job` | жизненный цикл, иерархия, отмена |
|
||||
| `CoroutineDispatcher` | на каком потоке выполнять |
|
||||
| `CoroutineName` | имя для отладки и логов |
|
||||
| `CoroutineExceptionHandler` | что делать с необработанной ошибкой |
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.ContinuationInterceptor
|
||||
|
||||
fun main() = runBlocking {
|
||||
val context = Dispatchers.IO + CoroutineName("задача") + SupervisorJob()
|
||||
|
||||
withContext(context) {
|
||||
println(coroutineContext[CoroutineName]?.name) // задача
|
||||
println(coroutineContext[ContinuationInterceptor] == Dispatchers.IO) // true
|
||||
}
|
||||
}
|
||||
```
|
||||
Сложение не коммутативно по смыслу: при конфликте элементов одного типа побеждает **правый**.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
fun main() {
|
||||
val base: CoroutineContext = Dispatchers.IO + CoroutineName("первое")
|
||||
val combined = base + CoroutineName("второе")
|
||||
println(combined[CoroutineName]?.name) // второе
|
||||
}
|
||||
```
|
||||
Для двух диспетчеров подряд (`Dispatchers.IO + Dispatchers.Default`) компилятор выдаёт предупреждение о бессмысленности выражения - правило то же, побеждает правый, но писать так незачем.
|
||||
|
||||
## Стандартные диспетчеры
|
||||
| Диспетчер | Потоков | Для чего |
|
||||
|---|---|---|
|
||||
| `Dispatchers.Default` | по числу ядер (минимум 2) | вычисления, обработка данных |
|
||||
| `Dispatchers.IO` | до 64 (растёт по требованию) | блокирующий I/O: файлы, JDBC, legacy-HTTP |
|
||||
| `Dispatchers.Main` | один, платформенный | UI (Android, Swing, JavaFX) |
|
||||
| `Dispatchers.Unconfined` | любой, текущий | редкие случаи, не для продакшена |
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
println("runBlocking: ${Thread.currentThread().name}")
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
println("Default: ${Thread.currentThread().name.contains("DefaultDispatcher")}")
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
println("IO: ${Thread.currentThread().name.contains("DefaultDispatcher")}")
|
||||
}
|
||||
// runBlocking: main
|
||||
// Default: true
|
||||
// IO: true
|
||||
}
|
||||
```
|
||||
Обратите внимание: и `Default`, и `IO` показывают `DefaultDispatcher-worker-N` - они **делят один пул потоков**. `IO` - это тот же пул с другим лимитом параллелизма, поэтому переключение между `Default` и `IO` дешёвое и не требует смены потока.
|
||||
|
||||
## Default против IO
|
||||
Выбор определяется одним вопросом: код считает или ждёт?
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
// CPU-bound: больше потоков, чем ядер, только вредит
|
||||
suspend fun compute(): Long = withContext(Dispatchers.Default) {
|
||||
(1..1_000_000).sumOf { it.toLong() }
|
||||
}
|
||||
|
||||
// блокирующий I/O: поток простаивает, поэтому их нужно много
|
||||
suspend fun readBlocking(): String = withContext(Dispatchers.IO) {
|
||||
Thread.sleep(20) // имитация JDBC или File.readText
|
||||
"прочитано"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(compute()) // 500000500000
|
||||
println(readBlocking()) // прочитано
|
||||
}
|
||||
```
|
||||
Логика лимитов: на `Default` потоков столько же, сколько ядер, потому что больше вычислять всё равно некому - лишние потоки только добавят переключений контекста. На `IO` потоки в основном спят в ожидании ответа, поэтому их держат до 64.
|
||||
|
||||
Если API уже `suspend` и не блокирует (Ktor-клиент, R2DBC, `kotlinx-io`), диспетчер менять не нужно вообще - это лишнее переключение.
|
||||
|
||||
## limitedParallelism
|
||||
Ограничивает число одновременных корутин, не создавая новых потоков:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
fun main() = runBlocking {
|
||||
val limited = Dispatchers.IO.limitedParallelism(2)
|
||||
val running = AtomicInteger(0)
|
||||
val maxRunning = AtomicInteger(0)
|
||||
|
||||
withContext(limited) {
|
||||
repeat(10) {
|
||||
launch {
|
||||
val now = running.incrementAndGet()
|
||||
maxRunning.updateAndGet { m -> maxOf(m, now) }
|
||||
Thread.sleep(20) // занимает поток
|
||||
running.decrementAndGet()
|
||||
}
|
||||
}
|
||||
}
|
||||
println("одновременно выполнялось не больше 2: ${maxRunning.get() <= 2}")
|
||||
// одновременно выполнялось не больше 2: true
|
||||
}
|
||||
```
|
||||
Это замена старому `newFixedThreadPoolContext`: вместо отдельного пула используется общий, а ограничивается только параллелизм. Типичное применение - не перегрузить базу данных или внешний API.
|
||||
|
||||
Важная тонкость: лимит считает корутины, **занимающие поток**, а не существующие. Приостановленная корутина слот освобождает:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
fun main() = runBlocking {
|
||||
val limited = Dispatchers.IO.limitedParallelism(2)
|
||||
val inFlight = AtomicInteger(0)
|
||||
val maxInFlight = AtomicInteger(0)
|
||||
|
||||
withContext(limited) {
|
||||
repeat(10) {
|
||||
launch {
|
||||
val now = inFlight.incrementAndGet()
|
||||
maxInFlight.updateAndGet { m -> maxOf(m, now) }
|
||||
delay(20) // слот освобождён
|
||||
inFlight.decrementAndGet()
|
||||
}
|
||||
}
|
||||
}
|
||||
println("одновременно в полёте больше 2: ${maxInFlight.get() > 2}")
|
||||
// одновременно в полёте больше 2: true
|
||||
}
|
||||
```
|
||||
Поэтому `limitedParallelism` - не семафор на число запросов. Если нужно ограничить именно количество одновременных обращений к внешнему API (включая время ожидания ответа), нужен `Semaphore` из `kotlinx.coroutines.sync`. Подробнее: [Разделяемое состояние](Разделяемое%20состояние.md)
|
||||
|
||||
## Dispatchers.Main
|
||||
```kotlin
|
||||
// Android или Swing — требует соответствующей зависимости
|
||||
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0")
|
||||
//
|
||||
// viewModelScope.launch { // уже на Main
|
||||
// val data = withContext(Dispatchers.IO) { repository.load() }
|
||||
// textView.text = data // снова на Main
|
||||
// }
|
||||
```
|
||||
Главный шаблон UI-программирования: корутина стартует на `Main`, тяжёлую работу уводит в `withContext`, результат обрабатывает снова на `Main` - без коллбэков и `runOnUiThread`. Без платформенной зависимости `Dispatchers.Main` бросит `IllegalStateException` при первом обращении.
|
||||
|
||||
## Unconfined
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
withContext(Dispatchers.Unconfined) {
|
||||
println("до delay: ${Thread.currentThread().name}")
|
||||
delay(10)
|
||||
println("после delay на потоке таймера: ${Thread.currentThread().name.contains("DefaultExecutor")}")
|
||||
}
|
||||
// до delay: main
|
||||
// после delay на потоке таймера: true
|
||||
}
|
||||
```
|
||||
`Unconfined` не привязывает корутину к потоку: до первой приостановки она идёт в текущем, а после возобновляется в том потоке, который её разбудил. Это ломает любые предположения о потоках, поэтому в продакшене не используется - только в тестах и специальных случаях.
|
||||
|
||||
## Наследование контекста
|
||||
Дочерняя корутина наследует контекст родителя, заменяя только `Job`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.ContinuationInterceptor
|
||||
|
||||
fun main() = runBlocking {
|
||||
withContext(Dispatchers.IO + CoroutineName("родитель")) {
|
||||
launch {
|
||||
println("имя: ${coroutineContext[CoroutineName]?.name}") // имя: родитель
|
||||
val inherited = coroutineContext[ContinuationInterceptor] == Dispatchers.IO
|
||||
println("диспетчер унаследован: $inherited") // диспетчер унаследован: true
|
||||
}
|
||||
launch(CoroutineName("ребёнок")) {
|
||||
println("переопределено: ${coroutineContext[CoroutineName]?.name}") // переопределено: ребёнок
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
`Job` ребёнка всегда новый и становится дочерним по отношению к `Job` родителя - именно так строится дерево. Подробнее: [Структурированная конкурентность](Структурированная%20конкурентность.md)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Контекст родителя:<br/>Job#1 + Dispatchers.IO + Name"] --> B["launch { }"]
|
||||
B --> C["Контекст ребёнка:<br/>Job#2 (parent = Job#1)<br/>+ Dispatchers.IO + Name"]
|
||||
A --> D["launch(Dispatchers.Default) { }"]
|
||||
D --> E["Контекст ребёнка:<br/>Job#3 (parent = Job#1)<br/>+ Dispatchers.Default + Name"]
|
||||
```
|
||||
|
||||
## Свои элементы контекста
|
||||
Элемент контекста - способ передать данные через всё дерево корутин:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.AbstractCoroutineContextElement
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
class RequestId(val value: String) : AbstractCoroutineContextElement(RequestId) {
|
||||
companion object Key : CoroutineContext.Key<RequestId>
|
||||
}
|
||||
|
||||
suspend fun logWithId(message: String) {
|
||||
val id = coroutineContext[RequestId]?.value ?: "нет id"
|
||||
println("[$id] $message")
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
withContext(RequestId("req-42")) {
|
||||
logWithId("начало обработки")
|
||||
launch { logWithId("из дочерней корутины") }
|
||||
}
|
||||
logWithId("вне контекста")
|
||||
// [req-42] начало обработки
|
||||
// [req-42] из дочерней корутины
|
||||
// [нет id] вне контекста
|
||||
}
|
||||
```
|
||||
Это корутинный аналог `ThreadLocal`, но работающий правильно при переключении потоков. Применяется для трассировки запросов, идентификаторов пользователя, локали.
|
||||
|
||||
## ThreadLocal и корутины
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
val threadLocal = ThreadLocal<String?>()
|
||||
|
||||
fun main() = runBlocking {
|
||||
threadLocal.set("главный")
|
||||
println("до: ${threadLocal.get()}")
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
println("на другом потоке: ${threadLocal.get()}") // null — другой поток
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Default + threadLocal.asContextElement("перенесённый")) {
|
||||
println("через asContextElement: ${threadLocal.get()}") // перенесённый
|
||||
}
|
||||
// до: главный
|
||||
// на другом потоке: null
|
||||
// через asContextElement: перенесённый
|
||||
}
|
||||
```
|
||||
`ThreadLocal` привязан к потоку, а корутина потоки меняет - поэтому напрямую он не работает. `asContextElement()` переносит значение при каждом переключении. Это важно для MDC в логировании и Spring Security.
|
||||
|
||||
## Проверка потока в отладке
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking(CoroutineName("корень")) {
|
||||
println(coroutineContext[CoroutineName]?.name) // корень
|
||||
launch(CoroutineName("дочерняя")) {
|
||||
println(coroutineContext[CoroutineName]?.name) // дочерняя
|
||||
}.join()
|
||||
}
|
||||
```
|
||||
Запуск с `-Dkotlinx.coroutines.debug` добавляет к имени потока идентификатор корутины (`main @корень#1`), что сильно упрощает чтение логов.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `ExecutorService` передаётся аргументом | диспетчер наследуется из контекста |
|
||||
> | `Executors.newFixedThreadPool(n)` | `Dispatchers.Default` / `limitedParallelism(n)` |
|
||||
> | `Executors.newCachedThreadPool()` | `Dispatchers.IO` |
|
||||
> | `SwingUtilities.invokeLater`, `runOnUiThread` | `Dispatchers.Main` |
|
||||
> | `ThreadLocal` | элемент контекста |
|
||||
> | `MDC` в логировании | `MDCContext` или свой элемент |
|
||||
> | пул = потоки | диспетчер = политика, потоки общие |
|
||||
>
|
||||
> **Пул потоков против диспетчера.** В Java каждый `ExecutorService` владеет своими потоками: четыре пула по 10 потоков - это 40 реальных потоков, даже если 35 из них простаивают. `Dispatchers.Default` и `Dispatchers.IO` делят один пул, а `limitedParallelism` ограничивает только число одновременных задач. Поэтому в Kotlin «ограничить параллелизм до 2 для работы с БД» не стоит двух простаивающих потоков.
|
||||
>
|
||||
> **Контекст наследуется, пул - нет.** В Java пул нужно передавать в каждый асинхронный вызов, и забытый аргумент молча уводит задачу в `ForkJoinPool.commonPool`:
|
||||
> ```java
|
||||
> CompletableFuture.supplyAsync(this::load); // commonPool!
|
||||
> CompletableFuture.supplyAsync(this::load, myExecutor); // правильно
|
||||
> ```
|
||||
> Эти две строки отличаются одним аргументом и дают совершенно разное поведение под нагрузкой. В Kotlin дочерняя корутина получает диспетчер родителя автоматически, а переопределение - явное и видимое.
|
||||
>
|
||||
> **`ThreadLocal` в асинхронном коде в Java не работает так же** - и там это обходят сложнее. При передаче задачи в пул `ThreadLocal` теряется; Spring решает это через `TaskDecorator` и копирование `SecurityContext`/MDC в каждом месте. В Kotlin элемент контекста путешествует вместе с корутиной по всему дереву, а `asContextElement()` переносит и настоящий `ThreadLocal`, когда он нужен библиотеке.
|
||||
>
|
||||
> **Переключение потока явное и дешёвое.** В Java чтобы вернуться в UI-поток, нужен `invokeLater` или `Handler.post` с коллбэком, который разрывает линейность кода. `withContext(Dispatchers.Main)` - это обычный блок, после которого выполнение продолжается на нужном потоке:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> suspend fun loadAndRender(): String {
|
||||
> val data = withContext(Dispatchers.IO) { Thread.sleep(10); "данные" }
|
||||
> return "отрисовано: $data" // снова на исходном диспетчере
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking { println(loadAndRender()) } // отрисовано: данные
|
||||
> ```
|
||||
>
|
||||
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Блокирующий вызов на `Dispatchers.Default` исчерпывает пул:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlin.system.measureTimeMillis
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val t = measureTimeMillis {
|
||||
> withContext(Dispatchers.Default.limitedParallelism(2)) {
|
||||
> List(6) { launch { Thread.sleep(50) } }.forEach { it.join() }
|
||||
> }
|
||||
> }
|
||||
> println("сериализовалось вместо параллели: ${t >= 150}") // сериализовалось вместо параллели: true
|
||||
> }
|
||||
> ```
|
||||
> Шесть задач по 50 мс на двух потоках заняли 150 мс вместо 50. На реальном `Default` (число ядер) блокирующий код точно так же съедает все потоки, и вся программа встаёт - включая несвязанные корутины.
|
||||
>
|
||||
> **`Dispatchers.IO` не делает блокирующий код неблокирующим.** Он лишь даёт больше потоков. 64 одновременных JDBC-запроса всё равно упрутся в размер пула соединений, а 1000 корутин на `IO` встанут в очередь.
|
||||
>
|
||||
> **`Dispatchers.Main` падает без платформенной зависимости:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() {
|
||||
> val error = runCatching { runBlocking { withContext(Dispatchers.Main) { } } }
|
||||
> .exceptionOrNull()
|
||||
> println(error != null) // true
|
||||
> }
|
||||
> ```
|
||||
> Сообщение - «Module with the Main dispatcher is missing». Нужен `kotlinx-coroutines-android`, `-javafx` или `-swing`.
|
||||
>
|
||||
> **`newSingleThreadContext` создаёт настоящий поток, который нужно закрывать:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> @OptIn(DelicateCoroutinesApi::class)
|
||||
> val dispatcher = newSingleThreadContext("мой-поток")
|
||||
> try {
|
||||
> withContext(dispatcher) {
|
||||
> println(Thread.currentThread().name) // мой-поток
|
||||
> }
|
||||
> } finally {
|
||||
> dispatcher.close() // без этого поток живёт до конца процесса
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
> API помечено `@DelicateCoroutinesApi` именно из-за этой утечки. Почти всегда вместо него нужен `limitedParallelism(1)`.
|
||||
>
|
||||
> **`Unconfined` возобновляется в неизвестном потоке.** Код после `delay` окажется на потоке таймера, после `Channel.receive` - на потоке отправителя. Любая работа с UI или `ThreadLocal` при этом ломается непредсказуемо.
|
||||
>
|
||||
> **`withContext` внутри цикла - лишние переключения:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlin.system.measureTimeMillis
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val inside = measureTimeMillis {
|
||||
> repeat(10_000) { withContext(Dispatchers.Default) { it } }
|
||||
> }
|
||||
> val outside = measureTimeMillis {
|
||||
> withContext(Dispatchers.Default) { repeat(10_000) { } }
|
||||
> }
|
||||
> println("снаружи быстрее: ${outside < inside}") // снаружи быстрее: true
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Передача `Job` в `withContext` рвёт структурированность.** `withContext(Dispatchers.IO + Job())` создаёт корутину без родителя - отмена сверху до неё не дойдёт. Передавайте только диспетчер и имя.
|
||||
>
|
||||
> **Контекст неизменяем, `+` создаёт новый.** `context + CoroutineName("x")` не меняет исходный объект; забытое присваивание результата - тихая ошибка.
|
||||
>
|
||||
> **`ThreadLocal` без `asContextElement` теряется при любом переключении**, в том числе при возобновлении после `delay` на другом потоке. Для MDC в логах это значит, что идентификатор запроса исчезнет из части записей - используйте `MDCContext` из `kotlinx-coroutines-slf4j`.
|
||||
>
|
||||
> **Число потоков `IO` настраивается только через системное свойство** (`kotlinx.coroutines.io.parallelism`) и влияет на весь процесс. Для ограничения конкретного места используйте `limitedParallelism`.
|
||||
>
|
||||
> **Переключение диспетчера для неблокирующего API - чистая потеря.** Если библиотека уже `suspend` и не блокирует, `withContext(Dispatchers.IO)` вокруг неё добавляет переключение контекста и ничего не даёт.
|
||||
|
||||
Далее: [Отмена и таймауты](Отмена%20и%20таймауты.md)
|
||||
@@ -0,0 +1,529 @@
|
||||
---
|
||||
title: Исключения в корутинах
|
||||
description: Как ошибки идут по дереву, разница launch и async, SupervisorJob, CoroutineExceptionHandler и CancellationException
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
Обработка ошибок в корутинах отличается от обычной именно потому, что корутина - не вызов функции: исключение возникает не там, где его ждёт `try`, а внутри другой корутины.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val result = runCatching {
|
||||
coroutineScope {
|
||||
launch { throw IllegalStateException("ошибка внутри") }
|
||||
}
|
||||
}
|
||||
println(result.exceptionOrNull()?.message) // ошибка внутри
|
||||
}
|
||||
```
|
||||
Ошибка из `launch` не выбрасывается в месте вызова `launch` - она идёт **вверх по дереву** и проявляется на границе области.
|
||||
|
||||
## Два механизма: launch и async
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
// launch: ошибка идёт родителю сразу
|
||||
val fromLaunch = runCatching {
|
||||
coroutineScope { launch { error("из launch") } }
|
||||
}.exceptionOrNull()?.message
|
||||
|
||||
// async: ошибка хранится в Deferred и выбрасывается при await
|
||||
val fromAsync = runCatching {
|
||||
coroutineScope {
|
||||
val d = async { error("из async") }
|
||||
d.await()
|
||||
}
|
||||
}.exceptionOrNull()?.message
|
||||
|
||||
println("$fromLaunch | $fromAsync") // из launch | из async
|
||||
}
|
||||
```
|
||||
Разница в том, **когда** вы увидите ошибку:
|
||||
|
||||
| | `launch` | `async` |
|
||||
|---|---|---|
|
||||
| куда идёт ошибка | сразу родителю | сохраняется в `Deferred` |
|
||||
| где ловить | `try` вокруг области, обработчик | `try` вокруг `await()` |
|
||||
| если не обработать | падает область | при `coroutineScope` - тоже падает область |
|
||||
| если `async` висит в supervisor | - | ошибка потеряется совсем |
|
||||
|
||||
Ключевой момент: `async` в обычной области **всё равно** передаёт ошибку родителю, даже без `await()`. Исключение в этом случае попадает в родителя при завершении области.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val result = runCatching {
|
||||
coroutineScope {
|
||||
async { error("await не вызывали") }
|
||||
delay(20)
|
||||
"тело дошло до конца"
|
||||
}
|
||||
}
|
||||
println(result.exceptionOrNull()?.message) // await не вызывали
|
||||
}
|
||||
```
|
||||
А вот в supervisor-области непрочитанная ошибка `async` исчезает:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
supervisorScope {
|
||||
async { error("никто не узнает") } // await не вызван
|
||||
delay(20)
|
||||
println("ни падения, ни лога")
|
||||
}
|
||||
// ни падения, ни лога
|
||||
}
|
||||
```
|
||||
Это самый тихий способ потерять ошибку в корутинах.
|
||||
|
||||
## Как ошибка идёт по дереву
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val result = runCatching {
|
||||
coroutineScope {
|
||||
launch { // уровень 1
|
||||
launch { // уровень 2
|
||||
launch { // уровень 3
|
||||
throw IllegalStateException("с третьего уровня")
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
try { delay(1000) } finally { println("соседнюю ветку отменили") }
|
||||
}
|
||||
}
|
||||
}
|
||||
println("дошло до корня: ${result.exceptionOrNull()?.message}")
|
||||
// соседнюю ветку отменили
|
||||
// дошло до корня: с третьего уровня
|
||||
}
|
||||
```
|
||||
Механика: корутина, получившая ошибку, отменяет себя, передаёт исключение родителю, тот отменяет **всех остальных детей** и повторяет процедуру выше. Промежуточные уровни не могут «не пропустить» ошибку - её нужно ловить `try/catch` внутри самой корутины.
|
||||
|
||||
```mermaid
|
||||
graph BT
|
||||
D["Ребёнок уровня 3<br/>throw"] -->|"ошибка наверх"| C["Ребёнок уровня 2"]
|
||||
C -->|"ошибка наверх"| B["Ребёнок уровня 1"]
|
||||
B -->|"ошибка наверх"| A["Корень области"]
|
||||
A -->|"отменить"| E["Соседняя ветка"]
|
||||
A --> F{"есть обработчик?"}
|
||||
F -->|"да"| G["CoroutineExceptionHandler"]
|
||||
F -->|"нет"| H["Thread.uncaughtExceptionHandler<br/>падение"]
|
||||
```
|
||||
|
||||
## Где try/catch работает
|
||||
Внутри самой корутины - как обычно:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
launch {
|
||||
try {
|
||||
error("локальная ошибка")
|
||||
} catch (e: IllegalStateException) {
|
||||
println("обработали на месте: ${e.message}")
|
||||
}
|
||||
}.join()
|
||||
println("родитель не затронут")
|
||||
// обработали на месте: локальная ошибка
|
||||
// родитель не затронут
|
||||
}
|
||||
```
|
||||
А вокруг `launch` - нет:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch(SupervisorJob()) { // отдельный Job, чтобы не уронить main
|
||||
try {
|
||||
launch { error("из вложенного launch") }
|
||||
} catch (e: Exception) {
|
||||
println("этого не будет")
|
||||
}
|
||||
}
|
||||
job.join()
|
||||
println("родитель выжил: ${job.isCancelled}")
|
||||
// родитель выжил: true
|
||||
}
|
||||
```
|
||||
`launch` возвращается мгновенно, до того как тело начало выполняться - ловить там нечего.
|
||||
|
||||
## SupervisorJob
|
||||
Супервизор меняет направление: ошибка ребёнка не идёт вверх.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val handler = CoroutineExceptionHandler { _, e ->
|
||||
println("обработчик: ${e.message}")
|
||||
}
|
||||
val scope = CoroutineScope(SupervisorJob() + handler)
|
||||
|
||||
scope.launch { error("первая задача упала") }
|
||||
delay(20)
|
||||
scope.launch { println("вторая работает как ни в чём не бывало") }
|
||||
delay(20)
|
||||
println("scope жив: ${scope.isActive}")
|
||||
scope.cancel()
|
||||
// обработчик: первая задача упала
|
||||
// вторая работает как ни в чём не бывало
|
||||
// scope жив: true
|
||||
}
|
||||
```
|
||||
Сравните с обычным `Job`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val handler = CoroutineExceptionHandler { _, e -> println("обработчик: ${e.message}") }
|
||||
val scope = CoroutineScope(Job() + handler)
|
||||
|
||||
scope.launch { error("первая задача упала") }
|
||||
delay(20)
|
||||
val second = scope.launch { println("не запустится") }
|
||||
delay(20)
|
||||
println("scope мёртв: ${!scope.isActive}, вторая отменена: ${second.isCancelled}")
|
||||
// обработчик: первая задача упала
|
||||
// scope мёртв: true, вторая отменена: true
|
||||
}
|
||||
```
|
||||
Правило: долгоживущий scope компонента - `SupervisorJob`; группа задач внутри одной операции - обычный `Job` через `coroutineScope`.
|
||||
|
||||
## supervisorScope против coroutineScope
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun loadAllOrFail(): String = coroutineScope {
|
||||
val a = async { delay(20); "A" }
|
||||
val b = async { error("B упал"); "B" }
|
||||
"${a.await()}${b.await()}"
|
||||
}
|
||||
|
||||
suspend fun loadWhatCan(): List<String> = supervisorScope {
|
||||
val a = async { delay(20); "A" }
|
||||
val b = async<String> { error("B упал") }
|
||||
listOf(
|
||||
runCatching { a.await() }.getOrElse { "?" },
|
||||
runCatching { b.await() }.getOrElse { "?" },
|
||||
)
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(runCatching { loadAllOrFail() }.exceptionOrNull()?.message) // B упал
|
||||
println(loadWhatCan()) // [A, ?]
|
||||
}
|
||||
```
|
||||
Первый вариант - «нужны все данные», второй - «покажем что есть». В supervisor-области каждый `await()` нужно обрабатывать отдельно, иначе ошибка улетит в обработчик по умолчанию.
|
||||
|
||||
## CoroutineExceptionHandler
|
||||
Это «последняя линия» - обработчик необработанных ошибок, аналог `Thread.UncaughtExceptionHandler`.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val handler = CoroutineExceptionHandler { context, e ->
|
||||
println("необработанная ошибка в ${context[CoroutineName]?.name}: ${e.message}")
|
||||
}
|
||||
val scope = CoroutineScope(SupervisorJob() + handler)
|
||||
|
||||
scope.launch(CoroutineName("загрузчик")) { error("сеть недоступна") }
|
||||
delay(20)
|
||||
scope.cancel()
|
||||
// необработанная ошибка в загрузчик: сеть недоступна
|
||||
}
|
||||
```
|
||||
Три правила, которые не очевидны:
|
||||
|
||||
**1. Обработчик работает только в корневой корутине.** В дочерней он игнорируется:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val handler = CoroutineExceptionHandler { _, e -> println("вызван: ${e.message}") }
|
||||
val scope = CoroutineScope(SupervisorJob())
|
||||
|
||||
scope.launch {
|
||||
launch(handler) { error("ошибка в ребёнке") } // handler проигнорирован
|
||||
}
|
||||
delay(50)
|
||||
scope.cancel()
|
||||
// Exception in thread ... IllegalStateException: ошибка в ребёнке
|
||||
}
|
||||
```
|
||||
Ошибка ушла наверх, а наверху обработчика нет. Ставить `handler` нужно в контекст scope или корневого билдера.
|
||||
|
||||
**2. `async` обработчик не использует** - его ошибка принадлежит `Deferred`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val handler = CoroutineExceptionHandler { _, e -> println("не будет вызван") }
|
||||
val scope = CoroutineScope(SupervisorJob() + handler)
|
||||
|
||||
val d = scope.async<Int> { error("ошибка в async") }
|
||||
delay(20)
|
||||
println("ошибка ждёт в Deferred: ${runCatching { d.await() }.isFailure}")
|
||||
scope.cancel()
|
||||
// ошибка ждёт в Deferred: true
|
||||
}
|
||||
```
|
||||
|
||||
**3. `runBlocking` обработчик не использует** - он выбрасывает исключение наружу, в вызывающий поток.
|
||||
|
||||
## CancellationException - не ошибка
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch {
|
||||
launch {
|
||||
delay(1000)
|
||||
}.cancel() // отмена ребёнка
|
||||
delay(20)
|
||||
println("родитель продолжает работу")
|
||||
}
|
||||
job.join()
|
||||
println("родитель завершился нормально: ${!job.isCancelled}")
|
||||
// родитель продолжает работу
|
||||
// родитель завершился нормально: true
|
||||
}
|
||||
```
|
||||
`CancellationException`, выброшенное корутиной, трактуется как «я завершилась по запросу» и наверх не передаётся. Именно поэтому `withTimeout` внутри одной корутины не роняет соседей. Подробнее: [Отмена и таймауты](Отмена%20и%20таймауты.md)
|
||||
|
||||
Обратная сторона: `CancellationException` нельзя использовать как обычное исключение - оно будет молча проигнорировано:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch(SupervisorJob()) {
|
||||
throw CancellationException("это не ошибка, а отмена")
|
||||
}
|
||||
job.join()
|
||||
println("отменён, а не упал: ${job.isCancelled}") // отменён, а не упал: true
|
||||
}
|
||||
```
|
||||
|
||||
## Агрегация ошибок
|
||||
Родитель получает **первое** исключение. Ошибки, возникшие уже во время отмены - например, в блоке `finally` соседней корутины - не теряются, а попадают в `suppressed`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val error = runCatching {
|
||||
coroutineScope {
|
||||
launch { delay(10); throw IllegalStateException("первая") }
|
||||
launch {
|
||||
try { delay(1000) } finally { throw IllegalArgumentException("из finally") }
|
||||
}
|
||||
}
|
||||
}.exceptionOrNull()
|
||||
|
||||
println("основная: ${error?.message}")
|
||||
println("подавленных: ${error?.suppressed?.size}")
|
||||
println("подавленная: ${error?.suppressed?.firstOrNull()?.message}")
|
||||
// основная: первая
|
||||
// подавленных: 1
|
||||
// подавленная: из finally
|
||||
}
|
||||
```
|
||||
Важно, чего здесь **нет**: если бы вторая корутина просто бросила исключение через 30 мс, подавленной ошибки не появилось бы - её отменили бы на 10-й миллисекунде, и она никогда не дошла бы до `throw`. Fail-fast означает именно это: вторая ошибка обычно не возникает, потому что работу остановили. В `suppressed` попадают только сбои очистки.
|
||||
|
||||
Практический вывод для логирования: печатать нужно полный стектрейс (`printStackTrace()` или `logger.error(msg, e)`), а не `e.message` - иначе сбои в `finally` останутся невидимыми.
|
||||
|
||||
## Повторные попытки
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun <T> retry(times: Int, delayMs: Long, block: suspend () -> T): T {
|
||||
var last: Throwable? = null
|
||||
repeat(times) { attempt ->
|
||||
try {
|
||||
return block()
|
||||
} catch (e: CancellationException) {
|
||||
throw e // отмену не ретраим!
|
||||
} catch (e: Exception) {
|
||||
last = e
|
||||
println("попытка ${attempt + 1} не удалась: ${e.message}")
|
||||
delay(delayMs)
|
||||
}
|
||||
}
|
||||
throw last ?: IllegalStateException("нет попыток")
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
var attempts = 0
|
||||
val result = retry(3, 10) {
|
||||
attempts++
|
||||
if (attempts < 3) error("сеть недоступна") else "данные получены"
|
||||
}
|
||||
println(result)
|
||||
// попытка 1 не удалась: сеть недоступна
|
||||
// попытка 2 не удалась: сеть недоступна
|
||||
// данные получены
|
||||
}
|
||||
```
|
||||
Строка `catch (e: CancellationException) { throw e }` обязательна в любом обобщённом обработчике ошибок в корутинах - без неё функция станет неотменяемой.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | исключение в `submit` → в `Future` | ошибка `launch` → родителю |
|
||||
> | `Future.get()` → `ExecutionException` | `await()` → исходное исключение |
|
||||
> | `CompletableFuture.exceptionally` | `try/catch` вокруг `await` |
|
||||
> | `Thread.UncaughtExceptionHandler` | `CoroutineExceptionHandler` |
|
||||
> | `InterruptedException` (проверяемое) | `CancellationException` (непроверяемое) |
|
||||
> | ошибка задачи не влияет на пул | ошибка отменяет область |
|
||||
> | нет аналога | `SupervisorJob` |
|
||||
>
|
||||
> **Ошибку в Java легко потерять молча.** `executor.submit(task)` складывает исключение в `Future`, и если `get()` никто не вызовет, об ошибке не узнает никто - ни логов, ни падения:
|
||||
> ```java
|
||||
> executor.submit(() -> { throw new RuntimeException("исчезнет"); });
|
||||
> // тишина
|
||||
> ```
|
||||
> Корутины этого не допускают: ошибка `launch` обязательно дойдёт до родителя, а если никто не обработал - до `CoroutineExceptionHandler` или `Thread.uncaughtExceptionHandler`. Исключение одно - непрочитанный `async` в supervisor-области, и это считается ошибкой программиста.
|
||||
>
|
||||
> **`get()` оборачивает исключение, `await()` - нет.** В Java настоящая причина лежит внутри `ExecutionException`, поэтому обработка выглядит так:
|
||||
> ```java
|
||||
> try {
|
||||
> future.get();
|
||||
> } catch (ExecutionException e) {
|
||||
> if (e.getCause() instanceof IOException io) { /* ... */ }
|
||||
> }
|
||||
> ```
|
||||
> В Kotlin `await()` бросает то же исключение, что было выброшено внутри, поэтому `catch (e: IOException)` работает напрямую:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import java.io.IOException
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val caught = try {
|
||||
> coroutineScope { async { throw IOException("диск") }.await() }
|
||||
> } catch (e: IOException) {
|
||||
> "поймали ${e::class.simpleName}: ${e.message}"
|
||||
> }
|
||||
> println(caught) // поймали IOException: диск
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`exceptionally`/`handle` против `try/catch`.** В Java обработка ошибок в асинхронной цепочке - отдельный метод с лямбдой, и правила распространения ошибки по цепочке приходится помнить (`exceptionally` ловит, `thenApply` пропускает, `whenComplete` не меняет результат). В Kotlin работает обычный `try/catch`, с теми же правилами, что в синхронном коде.
|
||||
>
|
||||
> **Отмена пула против отмены области.** В Java ошибка в одной задаче никак не влияет на остальные - пул продолжает работу. Это и плюс (изоляция), и минус (бессмысленное продолжение работы, часть которой уже провалилась). Kotlin даёт выбор явно: `coroutineScope` - fail-fast, `supervisorScope` - изоляция как в Java.
|
||||
>
|
||||
> **`CancellationException` непроверяемое, `InterruptedException` - нет.** В Java прерывание приходится обрабатывать в каждом блокирующем вызове, и типичная ошибка - проглотить его, не восстановив флаг. В Kotlin отмену не нужно объявлять, но проглотить её так же легко через `catch (e: Exception)`.
|
||||
>
|
||||
> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md), [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`try/catch` вокруг `launch` ничего не ловит:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch(SupervisorJob()) {
|
||||
> try {
|
||||
> launch { error("внутри") }
|
||||
> } catch (e: Exception) {
|
||||
> println("не напечатается")
|
||||
> }
|
||||
> delay(50)
|
||||
> }
|
||||
> job.join()
|
||||
> println("ошибка ушла наверх, а не в catch")
|
||||
> // ошибка ушла наверх, а не в catch
|
||||
> }
|
||||
> ```
|
||||
> `launch` возвращается до выполнения тела. Ловить нужно **внутри** лямбды или вокруг `coroutineScope`.
|
||||
>
|
||||
> **`catch (e: Exception)` перехватывает отмену:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch {
|
||||
> try {
|
||||
> delay(1000)
|
||||
> } catch (e: Exception) {
|
||||
> println("отмена попала в общий catch: ${e is CancellationException}")
|
||||
> }
|
||||
> println("и работа продолжилась")
|
||||
> }
|
||||
> delay(20)
|
||||
> job.cancelAndJoin()
|
||||
> // отмена попала в общий catch: true
|
||||
> // и работа продолжилась
|
||||
> }
|
||||
> ```
|
||||
> Всегда ставьте `catch (e: CancellationException) { throw e }` первым.
|
||||
>
|
||||
> **`runCatching` в корутинах опасен** по той же причине - он ловит `Throwable`, включая `CancellationException`. В `suspend`-коде это делает блок неотменяемым, и `cancel()` перестаёт работать. Используйте явный `try/catch` с пробросом отмены.
|
||||
>
|
||||
> **`CoroutineExceptionHandler` в дочерней корутине игнорируется.** Он учитывается только у корневой корутины области. Передавать его нужно при создании scope или в корневой `launch`.
|
||||
>
|
||||
> **`CoroutineExceptionHandler` не работает с `async`** - его ошибка принадлежит `Deferred` и ждёт `await()`. Если `await()` не вызван в supervisor-области, ошибка исчезает навсегда.
|
||||
>
|
||||
> **`SupervisorJob`, переданный в `launch`, не делает детей независимыми:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch(SupervisorJob()) { // supervisor — для ЭТОЙ корутины
|
||||
> launch { delay(10); error("первый ребёнок") }
|
||||
> launch {
|
||||
> try { delay(1000) } finally { println("второго тоже отменили") }
|
||||
> }
|
||||
> }
|
||||
> job.join()
|
||||
> // второго тоже отменили
|
||||
> }
|
||||
> ```
|
||||
> `SupervisorJob` в контексте билдера становится родителем самой корутины, но внутри её тела работает обычная область. Нужен `supervisorScope { }`.
|
||||
>
|
||||
> **`supervisorScope` не защищает код своего тела:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val caught = runCatching {
|
||||
> supervisorScope {
|
||||
> launch { delay(1000) }
|
||||
> error("ошибка в теле блока")
|
||||
> }
|
||||
> }.exceptionOrNull()?.message
|
||||
> println(caught) // ошибка в теле блока
|
||||
> }
|
||||
> ```
|
||||
> Supervisor меняет поведение только для детей.
|
||||
>
|
||||
> **Ошибка внутри `CoroutineExceptionHandler` роняет процесс.** Обработчик - последняя линия, выше его некуда. Логирование внутри него должно быть защищено от собственных сбоев.
|
||||
>
|
||||
> **`finally` может не выполниться при падении JVM.** Если необработанная ошибка дошла до `Thread.uncaughtExceptionHandler` и процесс завершается, часть блоков очистки в других корутинах может не успеть.
|
||||
>
|
||||
> **Подавленные исключения легко не заметить.** При нескольких одновременных ошибках `e.message` покажет только первую. В логах нужен полный `printStackTrace()` или `e.suppressed`.
|
||||
>
|
||||
> **Проброс через несколько уровней нельзя перехватить в середине.** Промежуточный `launch` не видит ошибку ребёнка как исключение - он просто отменяется. Чтобы обработать её на этом уровне, нужен `try/catch` вокруг `coroutineScope` внутри промежуточной корутины.
|
||||
>
|
||||
> **`CancellationException` как бизнес-ошибка не работает:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> class ValidationError(message: String) : CancellationException(message)
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch(SupervisorJob()) { throw ValidationError("неверные данные") }
|
||||
> job.join()
|
||||
> println("ошибка проигнорирована: отменён=${job.isCancelled}")
|
||||
> // ошибка проигнорирована: отменён=true
|
||||
> }
|
||||
> ```
|
||||
> Наследование от `CancellationException` превращает исключение в «тихую отмену»: ни обработчика, ни падения. Для бизнес-ошибок наследуйтесь от `Exception`.
|
||||
|
||||
Далее: [Flow](Flow.md)
|
||||
@@ -0,0 +1,344 @@
|
||||
---
|
||||
title: Корутины против потоков
|
||||
description: Реальная стоимость потока и корутины, сравнение с виртуальными потоками Java и случаи, когда нужны именно потоки
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
Поток - ресурс ОС: стек в памяти, запись в планировщике ядра, переключение через системный вызов. Корутина - объект в куче, переключение которого - обычный вызов метода.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun main() = runBlocking {
|
||||
val coroutines = measureTimeMillis {
|
||||
List(100_000) { launch { delay(100) } }.forEach { it.join() }
|
||||
}
|
||||
println("100 000 корутин: уложились в секунду: ${coroutines < 1000}")
|
||||
// 100 000 корутин: уложились в секунду: true
|
||||
}
|
||||
```
|
||||
Сто тысяч потоков создать нельзя - JVM упадёт с `OutOfMemoryError` ещё на нескольких десятках тысяч.
|
||||
|
||||
## Стоимость в цифрах
|
||||
| Характеристика | Поток ОС | Корутина |
|
||||
|---|---|---|
|
||||
| память | около 1 МБ стека (`-Xss`) | десятки-сотни байт |
|
||||
| создание | системный вызов, ~50-100 мкс | аллокация объекта, ~100 нс |
|
||||
| переключение | через ядро, 1-10 мкс | вызов метода, ~10 нс |
|
||||
| разумный максимум | тысячи | миллионы |
|
||||
| планировщик | ядро ОС | библиотека, в пользовательском коде |
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun main() = runBlocking {
|
||||
val threadTime = measureTimeMillis {
|
||||
List(1000) { Thread { Thread.sleep(10) } }.onEach { it.start() }.forEach { it.join() }
|
||||
}
|
||||
val coroutineTime = measureTimeMillis {
|
||||
List(1000) { launch { delay(10) } }.forEach { it.join() }
|
||||
}
|
||||
println("1000 потоков: ${threadTime} мс, 1000 корутин: ${coroutineTime} мс")
|
||||
println("корутины быстрее: ${coroutineTime < threadTime}")
|
||||
// корутины быстрее: true
|
||||
}
|
||||
```
|
||||
Разница здесь не в самом ожидании (оба спят по 10 мс), а в накладных расходах на создание тысячи объектов ядра.
|
||||
|
||||
## Где именно экономия
|
||||
Корутины не делают работу быстрее - они устраняют простой потоков.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun main() = runBlocking {
|
||||
// блокировка: 4 задачи на 1 потоке = 4 × 100 мс
|
||||
val blocking = measureTimeMillis {
|
||||
withContext(Dispatchers.Default.limitedParallelism(1)) {
|
||||
List(4) { launch { Thread.sleep(100) } }.forEach { it.join() }
|
||||
}
|
||||
}
|
||||
|
||||
// приостановка: 4 задачи на 1 потоке = 100 мс
|
||||
val suspending = measureTimeMillis {
|
||||
withContext(Dispatchers.Default.limitedParallelism(1)) {
|
||||
List(4) { launch { delay(100) } }.forEach { it.join() }
|
||||
}
|
||||
}
|
||||
|
||||
println("блокировка около 400 мс: ${blocking >= 380}")
|
||||
println("приостановка около 100 мс: ${suspending < 200}")
|
||||
// блокировка около 400 мс: true
|
||||
// приостановка около 100 мс: true
|
||||
}
|
||||
```
|
||||
Тот же один поток, та же работа - разница в четыре раза. Именно это окупает корутины на сервере, обслуживающем много одновременных запросов: поток не стоит в ожидании ответа базы, а берёт следующий запрос.
|
||||
|
||||
## Корутины не помогают при счёте
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun heavy(): Long = (1..20_000_000L).sum()
|
||||
|
||||
fun main() = runBlocking {
|
||||
val sequential = measureTimeMillis { repeat(4) { heavy() } }
|
||||
|
||||
val parallel = measureTimeMillis {
|
||||
withContext(Dispatchers.Default) {
|
||||
List(4) { async { heavy() } }.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
val oneThread = measureTimeMillis {
|
||||
withContext(Dispatchers.Default.limitedParallelism(1)) {
|
||||
List(4) { async { heavy() } }.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
println("параллельно на ядрах быстрее: ${parallel < sequential}")
|
||||
println("на одном потоке выигрыша нет: ${oneThread >= parallel}")
|
||||
// параллельно на ядрах быстрее: true
|
||||
// на одном потоке выигрыша нет: true
|
||||
}
|
||||
```
|
||||
Вывод: для вычислений выигрыш даёт только параллелизм по ядрам, а не количество корутин. Тысяча корутин на четырёх ядрах считает ровно столько же, сколько четыре. Корутины здесь удобны как способ организации кода, а не как ускорение.
|
||||
|
||||
## Что происходит при блокировке
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val cores = Runtime.getRuntime().availableProcessors()
|
||||
println("ядер в системе: ${cores > 0}")
|
||||
|
||||
val finished = withTimeoutOrNull(300) {
|
||||
withContext(Dispatchers.Default) {
|
||||
// занимаем все потоки Default блокирующими вызовами
|
||||
List(cores) { launch { Thread.sleep(500) } }
|
||||
launch { println("эта корутина не получит поток вовремя") }
|
||||
}
|
||||
true
|
||||
}
|
||||
println("диспетчер оказался парализован: ${finished == null}")
|
||||
// ядер в системе: true
|
||||
// диспетчер оказался парализован: true
|
||||
}
|
||||
```
|
||||
Это главный практический риск корутин: один блокирующий вызов в неправильном диспетчере останавливает несвязанные задачи. Поток, заблокированный на `Thread.sleep` или JDBC, не может выполнять другие корутины. Подробнее: [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md)
|
||||
|
||||
## Виртуальные потоки Java
|
||||
Project Loom решает ту же задачу иначе: делает блокировку дешёвой.
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
println("JVM: ${Runtime.version().feature()}")
|
||||
// Java 21+: Thread.ofVirtual().start { ... } — виртуальный поток,
|
||||
// который при блокирующем вызове освобождает несущий поток ОС.
|
||||
// Thread.sleep(1000) на нём стоит почти столько же, сколько delay(1000) в корутине.
|
||||
}
|
||||
```
|
||||
Сравнение подходов:
|
||||
|
||||
| | Корутины | Виртуальные потоки |
|
||||
|---|---|---|
|
||||
| где видно асинхронность | в типе: `suspend` | нигде, код обычный |
|
||||
| существующий блокирующий код | нужно переписать | работает как есть |
|
||||
| отмена | по дереву, встроена | `interrupt`, вручную |
|
||||
| структурированность | обязательна | `StructuredTaskScope`, опционально |
|
||||
| потоки данных | `Flow` | нет аналога, нужен Reactive Streams |
|
||||
| платформы | JVM, Android, Native, JS, Wasm | только JVM 21+ |
|
||||
| `ThreadLocal` | нужен `asContextElement` | работает (но дорог при миллионах) |
|
||||
| готовность экосистемы | библиотеки с `suspend` API | любая блокирующая библиотека |
|
||||
|
||||
**Главный плюс Loom** - обратная совместимость: миллионы строк блокирующего кода и библиотек (JDBC, Apache HttpClient, старые ORM) внезапно масштабируются без изменений.
|
||||
|
||||
**Главный плюс корутин** - асинхронность видна компилятору. Из `suspend` в сигнатуре вырастают структурированная конкурентность, отмена по дереву, `Flow` с backpressure и проверка «это нельзя вызвать из UI-потока» на этапе компиляции. С виртуальными потоками компилятор не знает, где функция заблокируется, поэтому такие гарантии недоступны.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
// в сигнатуре видно, что функция может приостановиться
|
||||
suspend fun loadUser(id: Int): String {
|
||||
delay(10)
|
||||
return "пользователь $id"
|
||||
}
|
||||
|
||||
// а здесь не видно ничего: может считать 1 нс, может ждать сеть 10 с
|
||||
fun loadUserBlocking(id: Int): String {
|
||||
Thread.sleep(10)
|
||||
return "пользователь $id"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(loadUser(1))
|
||||
println(loadUserBlocking(2))
|
||||
// пользователь 1
|
||||
// пользователь 2
|
||||
}
|
||||
```
|
||||
Второй вариант нельзя безопасно вызвать из UI-потока, и ни компилятор, ни тип этого не подскажут.
|
||||
|
||||
Для Android вопрос не стоит: виртуальных потоков там нет, корутины - стандарт. Для серверного Kotlin на JVM 21+ оба подхода рабочие, и выбор определяется экосистемой: Ktor и Exposed дают `suspend` API, Spring MVC и JDBC - блокирующий.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Нужна конкурентность"] --> B{"Платформа?"}
|
||||
B -->|"Android, Native, JS"| C["корутины"]
|
||||
B -->|"JVM 21+"| D{"Код и библиотеки?"}
|
||||
D -->|"блокирующие: JDBC,<br/>Spring MVC"| E["виртуальные потоки<br/>или корутины + IO"]
|
||||
D -->|"suspend API: Ktor,<br/>R2DBC"| C
|
||||
A --> F{"Нужны потоки данных,<br/>отмена по дереву?"}
|
||||
F -->|"да"| C
|
||||
```
|
||||
|
||||
## Когда нужны именно потоки
|
||||
Корутины не универсальны.
|
||||
|
||||
**1. Блокирующий код, который нельзя переписать.** JDBC, `java.io`, нативные библиотеки через JNI. Их место - `Dispatchers.IO` или отдельный пул:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun queryDatabase(): String = withContext(Dispatchers.IO) {
|
||||
Thread.sleep(20) // здесь мог быть JDBC
|
||||
"строка из БД"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(queryDatabase()) // строка из БД
|
||||
}
|
||||
```
|
||||
|
||||
**2. Код, требующий привязки к потоку.** Некоторые библиотеки используют `ThreadLocal` или требуют вызовов из одного и того же потока (OpenGL, часть UI-тулкитов, нативные SDK):
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
val dedicated = newSingleThreadContext("нативный-поток")
|
||||
try {
|
||||
withContext(dedicated) {
|
||||
println("всегда один и тот же поток: ${Thread.currentThread().name}")
|
||||
}
|
||||
} finally {
|
||||
dedicated.close()
|
||||
}
|
||||
// всегда один и тот же поток: нативный-поток
|
||||
}
|
||||
```
|
||||
|
||||
**3. Приоритеты и реальное время.** `Thread.setPriority`, потоки реального времени, привязка к ядру - у корутин этого нет вообще.
|
||||
|
||||
**4. Бесконечный цикл без точек приостановки.** Корутина, которая никогда не приостанавливается, занимает поток диспетчера полностью, и её нельзя отменить. Для такой задачи нужен выделенный поток.
|
||||
|
||||
## Измерение: сколько живёт корутина
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val before = Runtime.getRuntime().let { it.totalMemory() - it.freeMemory() }
|
||||
|
||||
val jobs = List(200_000) { launch { delay(5000) } }
|
||||
|
||||
val after = Runtime.getRuntime().let { it.totalMemory() - it.freeMemory() }
|
||||
val perCoroutine = (after - before) / 200_000
|
||||
|
||||
println("200 000 корутин создано: ${jobs.size}")
|
||||
println("байт на корутину (порядок величины): ${perCoroutine in 1..5000}")
|
||||
jobs.forEach { it.cancel() }
|
||||
// 200 000 корутин создано: 200000
|
||||
// байт на корутину (порядок величины): true
|
||||
}
|
||||
```
|
||||
Измерение грубое (GC работает параллельно), но порядок виден: сотни байт против мегабайта у потока. Именно поэтому «корутина на каждый запрос» или «корутина на каждое соединение» - нормальная архитектура, а «поток на каждый запрос» перестаёт работать на нескольких тысячах.
|
||||
|
||||
## Выбор на практике
|
||||
| Задача | Решение |
|
||||
|---|---|
|
||||
| много одновременных сетевых запросов | корутины |
|
||||
| обработка HTTP-запросов на сервере | корутины или виртуальные потоки |
|
||||
| асинхронность в Android | корутины, других вариантов нет |
|
||||
| параллельные вычисления | `Dispatchers.Default`, корутин по числу ядер |
|
||||
| JDBC и блокирующие драйверы | корутины + `Dispatchers.IO`, либо виртуальные потоки |
|
||||
| нативная библиотека с привязкой к потоку | выделенный поток |
|
||||
| задача с приоритетом ОС | поток |
|
||||
| поток событий с backpressure | `Flow` |
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `new Thread()` | `launch` |
|
||||
> | `ExecutorService` | `CoroutineScope` + диспетчер |
|
||||
> | `Thread.sleep` (блокирует) | `delay` (приостанавливает) |
|
||||
> | `Thread.ofVirtual()` (Java 21+) | корутины |
|
||||
> | `StructuredTaskScope` (Java 21+) | `coroutineScope` |
|
||||
> | `ThreadLocal` | элемент контекста корутины |
|
||||
> | «поток на запрос» не масштабируется | «корутина на запрос» - норма |
|
||||
>
|
||||
> **Модель «поток на запрос» и её предел.** Классический Java-сервер выделяет поток на каждое соединение. При 10 000 одновременных соединений это 10 000 потоков, около 10 ГБ стеков и планировщик ядра, занятый переключениями. Именно отсюда выросли NIO, Netty и реактивное программирование - с их колбэками и нечитаемыми стектрейсами. Корутины дают масштабируемость NIO, сохраняя линейный код:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> suspend fun handleRequest(id: Int): String {
|
||||
> delay(10) // здесь мог быть запрос к БД
|
||||
> return "ответ $id"
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val responses = List(10_000) { async { handleRequest(it) } }.awaitAll()
|
||||
> println("обработано запросов: ${responses.size}") // обработано запросов: 10000
|
||||
> }
|
||||
> ```
|
||||
> Десять тысяч «обработчиков запросов» на несколько потоков - в Java до Loom это требовало полностью асинхронного стека.
|
||||
>
|
||||
> **Виртуальные потоки - ответ Java на ту же проблему.** `Thread.ofVirtual().start { }` создаёт поток, который при блокирующем вызове освобождает несущий поток ОС. По стоимости это сравнимо с корутиной, а главное преимущество - существующий блокирующий код работает без изменений. Цена - отсутствие информации об асинхронности в типах, из-за чего нет ни отмены по дереву, ни `Flow`, ни проверок на этапе компиляции. Loom добирает часть этого отдельными API (`StructuredTaskScope`), но они опциональны и появились на 8 лет позже корутин.
|
||||
>
|
||||
> **`CompletableFuture` - промежуточное решение, которое проиграло оба раза.** Он даёт неблокирующий код, но ценой разрыва линейности: лямбды, протаскивание переменных через замыкания, неудобные `if` и циклы. Корутины и виртуальные потоки решают задачу так, что код остаётся последовательным - просто разными способами.
|
||||
>
|
||||
> **`ThreadLocal` стоит дорого при миллионе потоков.** Это ограничение Loom, о котором говорят меньше: `ThreadLocal` на миллион виртуальных потоков - это миллион копий значения. Для корутин элемент контекста наследуется деревом и копий не создаёт.
|
||||
>
|
||||
> **Отладка: стектрейсы.** Поток даёт полный стектрейс бесплатно - это заметный плюс и потоков, и виртуальных потоков. У корутины стектрейс содержит кадры только от последней точки возобновления, а историю приостановок восстанавливает библиотека `kotlinx-coroutines-debug` или флаг `-Dkotlinx.coroutines.debug`. Без них отладка асинхронного кода труднее, чем у потоков.
|
||||
>
|
||||
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Блокирующий вызов отменяет всю выгоду:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlin.system.measureTimeMillis
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val t = measureTimeMillis {
|
||||
> withContext(Dispatchers.Default.limitedParallelism(2)) {
|
||||
> List(10) { launch { Thread.sleep(50) } }.forEach { it.join() }
|
||||
> }
|
||||
> }
|
||||
> println("10 задач по 50 мс на 2 потоках: ${t >= 240}") // 10 задач по 50 мс на 2 потоках: true
|
||||
> }
|
||||
> ```
|
||||
> Корутины превратились в обычный пул потоков с очередью. Весь смысл - в `delay` вместо `Thread.sleep`, то есть в приостановке вместо блокировки.
|
||||
>
|
||||
> **«Корутин больше - значит быстрее» неверно для вычислений.** На четырёх ядрах сорок счётных корутин работают не быстрее четырёх, а переключений становится больше. Число параллельных вычислительных задач должно соответствовать числу ядер.
|
||||
>
|
||||
> **Стектрейсы хуже, чем у потоков.** Исключение из корутины содержит кадры от точки возобновления, а не всю логическую цепочку вызовов. Это реальная цена корутин, которую частично компенсирует `kotlinx-coroutines-debug`.
|
||||
>
|
||||
> **Корутина без точек приостановки не отменяется и занимает поток целиком.** Бесконечный счётный цикл в корутине хуже, чем в потоке: поток можно хотя бы прервать средствами ОС, а корутину - нет.
|
||||
>
|
||||
> **`ThreadLocal` не работает так, как ожидается.** Любая библиотека, полагающаяся на `ThreadLocal` (MDC в логах, `SecurityContext` в Spring, транзакции Hibernate), в корутинах требует `asContextElement` или специальной интеграции. С виртуальными потоками такой проблемы нет.
|
||||
>
|
||||
> **Профилировщики и инструменты знают о потоках, а не о корутинах.** В дампе потоков вы увидите `DefaultDispatcher-worker-1`, а не свои корутины; счётчики JMX считают потоки. Для корутин нужны отдельные инструменты - `kotlinx-coroutines-debug` или поддержка в IDE.
|
||||
>
|
||||
> **Переход на корутины не бесплатен архитектурно.** `suspend` поднимается по всему стеку вызовов до точки входа. Если наверху - не-suspend интерфейс (`Comparator`, `equals`, Java-колбэк), приостановку туда не поместить, и придётся либо менять архитектуру, либо блокировать поток через `runBlocking`.
|
||||
>
|
||||
> **Смешивание корутин и блокирующего кода даёт худшее из двух миров.** Половина кода на `suspend`, половина блокирующая, `runBlocking` в середине - такая система и не масштабируется, и сложна в отладке. Границу между блокирующим и приостанавливаемым кодом нужно держать явной и единственной.
|
||||
>
|
||||
> **Корутины не отменяют гонок и deadlock'ов.** Разделяемое изменяемое состояние требует синхронизации точно так же, как в потоках, а два `Mutex` в разном порядке дают взаимную блокировку. Подробнее: [Разделяемое состояние](Разделяемое%20состояние.md)
|
||||
>
|
||||
> **Измерения на микробенчмарках обманчивы.** JIT-компиляция, прогрев и GC искажают результаты в десятки раз. Цифры в этой заметке - порядки величин, а не точные значения; для реальных измерений нужен JMH.
|
||||
|
||||
Далее: [Взаимодействие с Java](../Взаимодействие%20с%20Java/Взаимодействие%20с%20Java.md)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,525 @@
|
||||
---
|
||||
title: Отмена и таймауты
|
||||
description: Кооперативная отмена, CancellationException, withTimeout, NonCancellable и освобождение ресурсов
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
Отмена корутин **кооперативная**: библиотека не может остановить код насильно, она лишь сообщает корутине, что её просят завершиться. Код должен это проверять - сам или через `suspend`-функции.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch {
|
||||
repeat(100) { i ->
|
||||
delay(10)
|
||||
println("шаг $i")
|
||||
}
|
||||
}
|
||||
delay(35)
|
||||
job.cancelAndJoin()
|
||||
println("отменено")
|
||||
// шаг 0
|
||||
// шаг 1
|
||||
// шаг 2
|
||||
// отменено
|
||||
}
|
||||
```
|
||||
Все `suspend`-функции из `kotlinx.coroutines` (`delay`, `join`, `await`, `withContext`) проверяют отмену и бросают `CancellationException`.
|
||||
|
||||
## Как работает отмена
|
||||
`cancel()` переводит `Job` в состояние Cancelling и бросает `CancellationException` в точке приостановки:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch {
|
||||
try {
|
||||
delay(1000)
|
||||
} catch (e: CancellationException) {
|
||||
println("поймали: ${e::class.simpleName}")
|
||||
throw e // обязательно пробросить!
|
||||
}
|
||||
}
|
||||
delay(20)
|
||||
job.cancelAndJoin()
|
||||
println("состояние: отменён=${job.isCancelled}, завершён=${job.isCompleted}")
|
||||
// поймали: JobCancellationException
|
||||
// состояние: отменён=true, завершён=true
|
||||
}
|
||||
```
|
||||
`CancellationException` - особое исключение: оно не считается ошибкой и не приводит к падению родителя. Подробнее: [Исключения в корутинах](Исключения%20в%20корутинах.md)
|
||||
|
||||
## Код без точек приостановки не отменяется
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun main() = runBlocking {
|
||||
val t = measureTimeMillis {
|
||||
val job = launch(Dispatchers.Default) {
|
||||
var sum = 0L
|
||||
repeat(200_000_000) { sum += it } // ни одной точки приостановки
|
||||
println("досчитал несмотря на отмену: ${sum > 0}")
|
||||
}
|
||||
delay(20)
|
||||
job.cancelAndJoin()
|
||||
}
|
||||
println("cancel ждал окончания цикла: ${t > 100}")
|
||||
// досчитал несмотря на отмену: true
|
||||
// cancel ждал окончания цикла: true
|
||||
}
|
||||
```
|
||||
Два способа сделать такой код отменяемым:
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
// 1. проверять isActive
|
||||
val job1 = launch(Dispatchers.Default) {
|
||||
var i = 0
|
||||
while (isActive && i < 200_000_000) i++
|
||||
println("вышел по isActive: ${i < 200_000_000}")
|
||||
}
|
||||
delay(20)
|
||||
job1.cancelAndJoin()
|
||||
|
||||
// 2. вызывать ensureActive() — бросит CancellationException
|
||||
val job2 = launch(Dispatchers.Default) {
|
||||
try {
|
||||
repeat(200_000_000) {
|
||||
if (it % 100_000 == 0) ensureActive()
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
println("вышел по ensureActive")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
delay(20)
|
||||
job2.cancelAndJoin()
|
||||
// вышел по isActive: true
|
||||
// вышел по ensureActive
|
||||
}
|
||||
```
|
||||
Есть и третий вариант - `yield()`: он и проверяет отмену, и даёт другим корутинам шанс выполниться.
|
||||
|
||||
| Способ | Бросает исключение | Когда применять |
|
||||
|---|---|---|
|
||||
| `isActive` | нет, возвращает `Boolean` | условие цикла |
|
||||
| `ensureActive()` | да, `CancellationException` | внутри тела, быстрый выход |
|
||||
| `yield()` | да | ещё и уступает поток другим |
|
||||
|
||||
## Освобождение ресурсов
|
||||
`finally` работает как обычно:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch {
|
||||
try {
|
||||
println("ресурс открыт")
|
||||
delay(1000)
|
||||
} finally {
|
||||
println("ресурс закрыт")
|
||||
}
|
||||
}
|
||||
delay(20)
|
||||
job.cancelAndJoin()
|
||||
println("готово")
|
||||
// ресурс открыт
|
||||
// ресурс закрыт
|
||||
// готово
|
||||
}
|
||||
```
|
||||
Но внутри `finally` отменённой корутины **нельзя приостанавливаться**:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch {
|
||||
try {
|
||||
delay(1000)
|
||||
} finally {
|
||||
val failed = runCatching { delay(50) }.isFailure
|
||||
println("delay в finally упал: $failed") // delay в finally упал: true
|
||||
}
|
||||
}
|
||||
delay(20)
|
||||
job.cancelAndJoin()
|
||||
}
|
||||
```
|
||||
Корутина уже отменена, поэтому любая приостановка немедленно бросает `CancellationException`. Если в очистке нужна приостановка - оберните её в `NonCancellable`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch {
|
||||
try {
|
||||
delay(1000)
|
||||
} finally {
|
||||
withContext(NonCancellable) {
|
||||
delay(20)
|
||||
println("асинхронная очистка выполнена")
|
||||
}
|
||||
}
|
||||
}
|
||||
delay(20)
|
||||
job.cancelAndJoin()
|
||||
println("готово")
|
||||
// асинхронная очистка выполнена
|
||||
// готово
|
||||
}
|
||||
```
|
||||
`NonCancellable` нужен именно и только для этого. Использовать его, чтобы «защитить» обычную работу от отмены - ошибка: такая корутина станет неостановимой.
|
||||
|
||||
Для `Closeable`-ресурсов проще `use`:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class Connection : AutoCloseable {
|
||||
fun query() = "результат"
|
||||
override fun close() = println("соединение закрыто")
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job = launch {
|
||||
Connection().use { conn ->
|
||||
println(conn.query())
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
delay(20)
|
||||
job.cancelAndJoin()
|
||||
// результат
|
||||
// соединение закрыто
|
||||
}
|
||||
```
|
||||
|
||||
## withTimeout
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun slowOperation(): String {
|
||||
delay(1000)
|
||||
return "готово"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
try {
|
||||
withTimeout(50) { slowOperation() }
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
println("не успели: ${e.message}")
|
||||
}
|
||||
// не успели: Timed out waiting for 50 ms
|
||||
}
|
||||
```
|
||||
`withTimeoutOrNull` вместо исключения возвращает `null` - обычно это удобнее:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val fast = withTimeoutOrNull(100) { delay(20); "успели" }
|
||||
val slow = withTimeoutOrNull(20) { delay(100); "не успели" }
|
||||
|
||||
println(fast) // успели
|
||||
println(slow) // null
|
||||
}
|
||||
```
|
||||
`TimeoutCancellationException` - подкласс `CancellationException`, поэтому он отменяет только свой блок, а не всю область.
|
||||
|
||||
## Таймаут и ресурсы
|
||||
Опасное место: таймаут может сработать между получением ресурса и входом в `try`.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class Resource2 {
|
||||
var closed = false
|
||||
fun close() { closed = true }
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
var resource: Resource2? = null
|
||||
withTimeoutOrNull(30) {
|
||||
resource = Resource2() // создали
|
||||
try {
|
||||
delay(1000) // здесь таймаут
|
||||
} finally {
|
||||
resource?.close() // закрыли
|
||||
}
|
||||
}
|
||||
println("ресурс закрыт: ${resource?.closed}") // ресурс закрыт: true
|
||||
}
|
||||
```
|
||||
Правило: открывайте ресурс **внутри** блока и закрывайте в `finally` или через `use`. Открытие снаружи с закрытием внутри - источник утечек.
|
||||
|
||||
## Отмена распространяется вниз
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val parent = launch {
|
||||
launch {
|
||||
try { delay(1000) } finally { println("ребёнок 1 завершён") }
|
||||
}
|
||||
launch {
|
||||
try { delay(1000) } finally { println("ребёнок 2 завершён") }
|
||||
}
|
||||
try { delay(1000) } finally { println("родитель завершён") }
|
||||
}
|
||||
delay(20)
|
||||
parent.cancelAndJoin()
|
||||
println("всё дерево остановлено")
|
||||
// ребёнок 1 завершён
|
||||
// ребёнок 2 завершён
|
||||
// родитель завершён
|
||||
// всё дерево остановлено
|
||||
}
|
||||
```
|
||||
Это бесплатное следствие структурированной конкурентности: отменив одну корутину, вы останавливаете всё поддерево. Подробнее: [Структурированная конкурентность](Структурированная%20конкурентность.md)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant В as Вызывающий
|
||||
participant Р as Родитель
|
||||
participant Д as Дети
|
||||
В->>Р: cancel()
|
||||
Р->>Р: состояние Cancelling
|
||||
Р->>Д: отменить всех детей
|
||||
Д->>Д: CancellationException<br/>в точке приостановки
|
||||
Д->>Д: выполнить finally
|
||||
Д-->>Р: ребёнок Cancelled
|
||||
Р->>Р: выполнить finally
|
||||
Р-->>В: состояние Cancelled, join() вернулся
|
||||
```
|
||||
|
||||
## cancel против cancelAndJoin
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val job1 = launch { try { delay(1000) } finally { println("1: очистка") } }
|
||||
job1.cancel() // не ждём завершения
|
||||
println("после cancel: завершён=${job1.isCompleted}")
|
||||
|
||||
val job2 = launch { try { delay(1000) } finally { println("2: очистка") } }
|
||||
job2.cancelAndJoin() // ждём
|
||||
println("после cancelAndJoin: завершён=${job2.isCompleted}")
|
||||
// после cancel: завершён=false
|
||||
// 1: очистка
|
||||
// 2: очистка
|
||||
// после cancelAndJoin: завершён=true
|
||||
}
|
||||
```
|
||||
`cancel()` только отправляет сигнал; `cancelAndJoin()` дожидается, пока корутина действительно остановится и выполнит очистку. Для корректного освобождения ресурсов нужен второй вариант.
|
||||
|
||||
## Отмена и async
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val deferred = async { delay(1000); "результат" }
|
||||
delay(20)
|
||||
deferred.cancel()
|
||||
|
||||
val error = runCatching { deferred.await() }.exceptionOrNull()
|
||||
println("await после cancel бросил: ${error is CancellationException}") // await после cancel бросил: true
|
||||
}
|
||||
```
|
||||
Отменённый `Deferred` при `await()` бросает `CancellationException`, а не возвращает `null`. Для «результат или ничего» используйте `withTimeoutOrNull`.
|
||||
|
||||
## Таймаут на весь scope
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun loadWithDeadline(): List<String>? = withTimeoutOrNull(60) {
|
||||
coroutineScope {
|
||||
val a = async { delay(20); "быстрый" }
|
||||
val b = async { delay(1000); "медленный" }
|
||||
listOf(a.await(), b.await())
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(loadWithDeadline()) // null — медленный не успел, отменились оба
|
||||
}
|
||||
```
|
||||
Таймаут поверх `coroutineScope` даёт дедлайн на всю группу задач: не успел один - отменяются все. Это правильное поведение для запроса с общим SLA.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `Thread.interrupt()` | `job.cancel()` |
|
||||
> | `Thread.isInterrupted()` | `isActive` |
|
||||
> | `InterruptedException` | `CancellationException` |
|
||||
> | `Future.cancel(true)` - одна задача | отмена всего поддерева |
|
||||
> | `ExecutorService.shutdownNow()` | `scope.cancel()` |
|
||||
> | `future.get(50, MILLISECONDS)` | `withTimeout(50)` |
|
||||
> | `ScheduledExecutorService` для дедлайна | `withTimeout` |
|
||||
> | флаг `volatile boolean stopped` | `isActive` |
|
||||
>
|
||||
> **Прерывание в Java тоже кооперативное, но работает хуже.** `Thread.interrupt()` ставит флаг и прерывает только блокирующие вызовы, которые умеют его замечать (`sleep`, `wait`, `join`, часть NIO). Проблемы знакомые: `InterruptedException` - проверяемое исключение, которое приходится ловить в каждом блокирующем вызове, а при перехвате легко потерять флаг:
|
||||
> ```java
|
||||
> try {
|
||||
> Thread.sleep(1000);
|
||||
> } catch (InterruptedException e) {
|
||||
> // забыли Thread.currentThread().interrupt() — прерывание потеряно
|
||||
> }
|
||||
> ```
|
||||
> В Kotlin `CancellationException` непроверяемое, поэтому его не нужно ловить везде, а `catch (e: Exception)` без проброса ломает отмену ровно так же - разница в том, что IDE и детекторы это подсказывают.
|
||||
>
|
||||
> **Отмена по дереву против отмены по задаче.** `Future.cancel(true)` прерывает один поток. Если задача породила подзадачи через другой `submit`, они продолжат работу, и отследить их можно только вручную - списком `Future`. В Kotlin `cancel()` на родителе гарантированно останавливает всё поддерево, включая задачи, запущенные тремя уровнями ниже.
|
||||
>
|
||||
> **Таймаут не требует отдельного планировщика.** В Java `future.get(50, MILLISECONDS)` прерывает **ожидание**, но не саму задачу - она продолжит выполняться в пуле. Чтобы остановить и её, нужен `cancel(true)` плюс поддержка прерывания в коде задачи. `withTimeout` отменяет сам блок:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> var finished = false
|
||||
> withTimeoutOrNull(30) {
|
||||
> try { delay(1000); finished = true } finally { println("задача остановлена") }
|
||||
> }
|
||||
> println("тело доработало: $finished")
|
||||
> // задача остановлена
|
||||
> // тело доработало: false
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`shutdownNow()` против `scope.cancel()`.** `shutdownNow` возвращает список незапущенных задач и прерывает потоки работающих - результат зависит от того, умеет ли код реагировать на прерывание. `scope.cancel()` отменяет корутины в известных точках приостановки, а `join()` даёт дождаться завершения очистки.
|
||||
>
|
||||
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Перехват `CancellationException` без проброса ломает отмену:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch {
|
||||
> repeat(5) {
|
||||
> try {
|
||||
> delay(20)
|
||||
> } catch (e: CancellationException) {
|
||||
> println("проглотили отмену на шаге $it")
|
||||
> // нет throw — цикл продолжается!
|
||||
> }
|
||||
> }
|
||||
> println("корутина доработала до конца")
|
||||
> }
|
||||
> delay(30)
|
||||
> job.cancelAndJoin()
|
||||
> // проглотили отмену на шаге 1
|
||||
> // проглотили отмену на шаге 2
|
||||
> // проглотили отмену на шаге 3
|
||||
> // проглотили отмену на шаге 4
|
||||
> // корутина доработала до конца
|
||||
> }
|
||||
> ```
|
||||
> Корутина стала неотменяемой. Та же беда возникает от `catch (e: Exception)` - `CancellationException` в него попадает.
|
||||
>
|
||||
> **`try/catch (Exception)` вокруг `suspend`-вызова перехватывает отмену:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch {
|
||||
> try {
|
||||
> delay(1000)
|
||||
> } catch (e: Exception) {
|
||||
> println("перехвачено как обычная ошибка: ${e is CancellationException}")
|
||||
> }
|
||||
> println("продолжили работу после отмены")
|
||||
> }
|
||||
> delay(20)
|
||||
> job.cancelAndJoin()
|
||||
> // перехвачено как обычная ошибка: true
|
||||
> // продолжили работу после отмены
|
||||
> }
|
||||
> ```
|
||||
> Правильно: сначала `catch (e: CancellationException) { throw e }`, затем `catch (e: Exception)`.
|
||||
>
|
||||
> **`runCatching` глотает `CancellationException`** по той же причине - он ловит `Throwable`. В корутинном коде это регулярная проблема: `runCatching { suspendCall() }` делает блок неотменяемым. Для `suspend`-кода нужен явный `try/catch` с пробросом отмены.
|
||||
>
|
||||
> **Приостановка в `finally` отменённой корутины не работает:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch {
|
||||
> try {
|
||||
> delay(1000)
|
||||
> } finally {
|
||||
> val ok = runCatching { delay(50); "очистка" }.isSuccess
|
||||
> println("приостановка в finally удалась: $ok") // приостановка в finally удалась: false
|
||||
> }
|
||||
> }
|
||||
> delay(20)
|
||||
> job.cancelAndJoin()
|
||||
> }
|
||||
> ```
|
||||
> Нужен `withContext(NonCancellable)`.
|
||||
>
|
||||
> **`NonCancellable` вокруг обычной работы делает корутину неостановимой:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlin.system.measureTimeMillis
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val t = measureTimeMillis {
|
||||
> val job = launch {
|
||||
> withContext(NonCancellable) {
|
||||
> delay(200) // отмена проигнорирована
|
||||
> println("доработал несмотря на cancel")
|
||||
> }
|
||||
> }
|
||||
> delay(20)
|
||||
> job.cancelAndJoin()
|
||||
> }
|
||||
> println("cancel ждал 200 мс: ${t >= 190}")
|
||||
> // доработал несмотря на cancel
|
||||
> // cancel ждал 200 мс: true
|
||||
> }
|
||||
> ```
|
||||
> `NonCancellable` оправдан только для короткой очистки в `finally`.
|
||||
>
|
||||
> **`cancel()` без `join()` не ждёт очистки.** Если после `cancel()` сразу закрыть ресурс или выйти из процесса, `finally` может не успеть выполниться. Используйте `cancelAndJoin()`.
|
||||
>
|
||||
> **Чистый вычислительный цикл игнорирует отмену.** Любой `while`/`repeat` без `suspend`-вызовов нужно дополнять `isActive` или `ensureActive()` - иначе корутина отработает до конца, а `cancelAndJoin()` будет её ждать.
|
||||
>
|
||||
> **`withTimeout` бросает исключение, которое легко не заметить:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val result = runCatching { withTimeout(20) { delay(100); "значение" } }
|
||||
> println("упало с таймаутом: ${result.exceptionOrNull() is TimeoutCancellationException}")
|
||||
> // упало с таймаутом: true
|
||||
> }
|
||||
> ```
|
||||
> `TimeoutCancellationException` - подкласс `CancellationException`, поэтому обычный `catch (e: CancellationException) { throw e }` его пробросит, и таймаут «не сработает» там, где вы ждали обработки. Используйте `withTimeoutOrNull` или ловите `TimeoutCancellationException` до отмены.
|
||||
>
|
||||
> **Ресурс, открытый до `withTimeout`, может утечь:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> var opened = 0
|
||||
> var closed = 0
|
||||
> withTimeoutOrNull(30) {
|
||||
> opened++ // открыли внутри — хорошо
|
||||
> try { delay(1000) } finally { closed++ }
|
||||
> }
|
||||
> println("открыто $opened, закрыто $closed") // открыто 1, закрыто 1
|
||||
> }
|
||||
> ```
|
||||
> Если бы `opened++` стоял перед `withTimeoutOrNull`, а `closed++` внутри - при срабатывании таймаута до входа в блок ресурс остался бы открытым.
|
||||
>
|
||||
> **Отменённый scope нельзя переиспользовать** - `launch` в нём сразу возвращает отменённый `Job`. Для перезапуска создавайте новый scope.
|
||||
>
|
||||
> **`await()` на отменённом `Deferred` бросает исключение, а не возвращает `null`.** Код, ожидающий `null` при отмене, получит падение.
|
||||
>
|
||||
> **Отмена не мгновенна.** Между `cancel()` и фактическим завершением проходит время: корутина должна дойти до точки приостановки и выполнить `finally`. Проверка `isCompleted` сразу после `cancel()` обычно даёт `false`.
|
||||
|
||||
Далее: [Исключения в корутинах](Исключения%20в%20корутинах.md)
|
||||
@@ -0,0 +1,516 @@
|
||||
---
|
||||
title: Разделяемое состояние
|
||||
description: Mutex, Semaphore, атомарные типы, конфайнмент потока и подход «не разделять состояние вообще»
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
Корутины не отменяют гонок. Если несколько корутин меняют одну переменную, нужна синхронизация - причём такая, которая приостанавливает, а не блокирует поток.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
var counter = 0
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { launch { repeat(1000) { counter++ } } }.forEach { it.join() }
|
||||
}
|
||||
println("потеряно инкрементов: ${counter < 100_000}") // потеряно инкрементов: true
|
||||
}
|
||||
```
|
||||
`counter++` - это чтение, увеличение и запись: три операции, между которыми может вклиниться другая корутина. На одном потоке проблемы не было бы, но `Dispatchers.Default` использует все ядра.
|
||||
|
||||
## Способы, по возрастанию цены
|
||||
| Подход | Стоимость | Когда применять |
|
||||
|---|---|---|
|
||||
| не разделять состояние | нулевая | всегда, когда возможно |
|
||||
| неизменяемые данные | нулевая | состояние целиком заменяется |
|
||||
| атомарные типы | очень низкая | счётчики, флаги, одна переменная |
|
||||
| `MutableStateFlow.update` | низкая | наблюдаемое состояние |
|
||||
| конфайнмент в один поток | низкая | группа связанных переменных |
|
||||
| `Mutex` | средняя | составная операция, вызов `suspend` внутри |
|
||||
| `Semaphore` | средняя | ограничение числа одновременных операций |
|
||||
|
||||
Порядок не случаен: первые два варианта убирают проблему, остальные её решают.
|
||||
|
||||
## Атомарные типы
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
fun main() = runBlocking {
|
||||
val counter = AtomicInteger(0)
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { launch { repeat(1000) { counter.incrementAndGet() } } }.forEach { it.join() }
|
||||
}
|
||||
println("точный результат: ${counter.get()}") // точный результат: 100000
|
||||
|
||||
val ref = AtomicReference(listOf<String>())
|
||||
withContext(Dispatchers.Default) {
|
||||
List(10) { i -> launch { ref.updateAndGet { it + "элемент $i" } } }.forEach { it.join() }
|
||||
}
|
||||
println("добавлено без потерь: ${ref.get().size}") // добавлено без потерь: 10
|
||||
}
|
||||
```
|
||||
Атомарные типы работают через CAS (compare-and-set) - никаких блокировок, только повтор при конфликте. Это самый дешёвый способ, но он покрывает только операции с **одной** переменной.
|
||||
|
||||
## Mutex
|
||||
`Mutex` - взаимное исключение для корутин. В отличие от `synchronized`, он приостанавливает, а не блокирует поток.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
fun main() = runBlocking {
|
||||
val mutex = Mutex()
|
||||
var counter = 0
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { launch { repeat(1000) { mutex.withLock { counter++ } } } }
|
||||
.forEach { it.join() }
|
||||
}
|
||||
println("результат точный: ${counter == 100_000}") // результат точный: true
|
||||
}
|
||||
```
|
||||
Главное преимущество - внутри `withLock` можно вызывать `suspend`-функции:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class Cache {
|
||||
private val mutex = Mutex()
|
||||
private val data = mutableMapOf<Int, String>()
|
||||
|
||||
suspend fun get(id: Int): String = mutex.withLock {
|
||||
data[id] ?: load(id).also { data[id] = it }
|
||||
}
|
||||
|
||||
private suspend fun load(id: Int): String {
|
||||
delay(10) // suspend внутри критической секции
|
||||
return "значение $id"
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val cache = Cache()
|
||||
val results = List(5) { async { cache.get(1) } }.awaitAll()
|
||||
println("все получили одно значение: ${results.toSet().size == 1}") // все получили одно значение: true
|
||||
}
|
||||
```
|
||||
С `synchronized` так нельзя: `suspend`-вызов внутри `synchronized`-блока не компилируется, потому что приостановка освободила бы поток, а монитор остался бы захваченным.
|
||||
|
||||
## Mutex не реентерабелен
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
fun main() = runBlocking {
|
||||
val mutex = Mutex()
|
||||
|
||||
val deadlocked = withTimeoutOrNull(100) {
|
||||
mutex.withLock {
|
||||
mutex.withLock { // вторичный захват тем же владельцем
|
||||
"недостижимо"
|
||||
}
|
||||
}
|
||||
}
|
||||
println("повторный захват повис: ${deadlocked == null}") // повторный захват повис: true
|
||||
}
|
||||
```
|
||||
Это осознанное решение: реентерабельность в корутинах определить корректно нельзя - «тот же владелец» для корутины понятие нечёткое, ведь она может перемещаться между потоками. Практический вывод: функция под `withLock` не должна вызывать другую функцию, которая берёт тот же мьютекс. Приватные методы, вызываемые из критической секции, не должны блокироваться сами.
|
||||
|
||||
## Semaphore
|
||||
Ограничивает число одновременных операций:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
fun main() = runBlocking {
|
||||
val semaphore = Semaphore(permits = 3)
|
||||
val inFlight = AtomicInteger(0)
|
||||
val maxInFlight = AtomicInteger(0)
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
List(20) {
|
||||
launch {
|
||||
semaphore.withPermit {
|
||||
val now = inFlight.incrementAndGet()
|
||||
maxInFlight.updateAndGet { m -> maxOf(m, now) }
|
||||
delay(20) // ожидание ответа API
|
||||
inFlight.decrementAndGet()
|
||||
}
|
||||
}
|
||||
}.forEach { it.join() }
|
||||
}
|
||||
println("одновременно не больше 3: ${maxInFlight.get() <= 3}") // одновременно не больше 3: true
|
||||
}
|
||||
```
|
||||
Важное отличие от `limitedParallelism`: семафор считает операции **в полёте**, включая приостановленные. Именно он нужен, чтобы не превысить лимит внешнего API. Подробнее: [Диспетчеры и контекст](Диспетчеры%20и%20контекст.md)
|
||||
|
||||
## Конфайнмент в один поток
|
||||
Если состояние меняется только в одном потоке, синхронизация не нужна вовсе:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun main() = runBlocking {
|
||||
val single = Dispatchers.Default.limitedParallelism(1)
|
||||
var counter = 0
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) {
|
||||
launch {
|
||||
repeat(1000) {
|
||||
withContext(single) { counter++ } // все изменения в одном потоке
|
||||
}
|
||||
}
|
||||
}.forEach { it.join() }
|
||||
}
|
||||
println("точный результат: ${counter == 100_000}") // точный результат: true
|
||||
}
|
||||
```
|
||||
Работает, но дорого: 100 000 переключений контекста. Правильная версия конфайнмента - **грубая**, когда весь блок работы уходит в один поток:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun main() = runBlocking {
|
||||
val single = Dispatchers.Default.limitedParallelism(1)
|
||||
var counter = 0
|
||||
|
||||
val coarse = measureTimeMillis {
|
||||
withContext(single) { // переключение один раз
|
||||
repeat(100_000) { counter++ }
|
||||
}
|
||||
}
|
||||
println("результат: $counter")
|
||||
println("грубый конфайнмент быстр: ${coarse < 500}")
|
||||
// результат: 100000
|
||||
// грубый конфайнмент быстр: true
|
||||
}
|
||||
```
|
||||
|
||||
## Актор: состояние внутри корутины
|
||||
Самый чистый подход - состояние не разделяется вообще, им владеет одна корутина:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
||||
sealed interface AccountCommand {
|
||||
data class Deposit(val amount: Int) : AccountCommand
|
||||
data class Withdraw(val amount: Int, val ok: CompletableDeferred<Boolean>) : AccountCommand
|
||||
data class Balance(val reply: CompletableDeferred<Int>) : AccountCommand
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val commands = Channel<AccountCommand>()
|
||||
|
||||
launch {
|
||||
var balance = 0 // никто больше не видит эту переменную
|
||||
for (cmd in commands) {
|
||||
when (cmd) {
|
||||
is AccountCommand.Deposit -> balance += cmd.amount
|
||||
is AccountCommand.Withdraw -> {
|
||||
val enough = balance >= cmd.amount
|
||||
if (enough) balance -= cmd.amount
|
||||
cmd.ok.complete(enough)
|
||||
}
|
||||
is AccountCommand.Balance -> cmd.reply.complete(balance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repeat(100) { commands.send(AccountCommand.Deposit(10)) }
|
||||
|
||||
val ok = CompletableDeferred<Boolean>()
|
||||
commands.send(AccountCommand.Withdraw(500, ok))
|
||||
println("списание прошло: ${ok.await()}")
|
||||
|
||||
val tooMuch = CompletableDeferred<Boolean>()
|
||||
commands.send(AccountCommand.Withdraw(10_000, tooMuch))
|
||||
println("превышение отклонено: ${!tooMuch.await()}")
|
||||
|
||||
val balance = CompletableDeferred<Int>()
|
||||
commands.send(AccountCommand.Balance(balance))
|
||||
println("баланс: ${balance.await()}")
|
||||
commands.close()
|
||||
// списание прошло: true
|
||||
// превышение отклонено: true
|
||||
// баланс: 500
|
||||
}
|
||||
```
|
||||
Проверка «хватает ли денег» и списание происходят атомарно без всякой блокировки - просто потому, что их выполняет одна корутина. Подробнее: [Channels](Channels.md)
|
||||
|
||||
## StateFlow для наблюдаемого состояния
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
fun main() = runBlocking {
|
||||
val state = MutableStateFlow(0)
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { launch { repeat(1000) { state.update { v -> v + 1 } } } }
|
||||
.forEach { it.join() }
|
||||
}
|
||||
println("результат точный: ${state.value == 100_000}") // результат точный: true
|
||||
}
|
||||
```
|
||||
`update` - это CAS-цикл, поэтому он и атомарен, и даёт оповещение подписчикам. Если состояние нужно наблюдать, это лучший вариант. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md)
|
||||
|
||||
## Как выбрать
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Состояние меняют<br/>несколько корутин"] --> B{"Можно вообще<br/>не разделять?"}
|
||||
B -->|"да"| C["локальная переменная<br/>или актор"]
|
||||
B -->|"нет"| D{"За состоянием<br/>наблюдают?"}
|
||||
D -->|"да"| E["MutableStateFlow<br/>+ update"]
|
||||
D -->|"нет"| F{"Одна переменная?"}
|
||||
F -->|"да"| G["AtomicInteger / AtomicReference"]
|
||||
F -->|"нет, составная операция"| H{"Нужен suspend<br/>внутри секции?"}
|
||||
H -->|"да"| I["Mutex.withLock"]
|
||||
H -->|"нет"| J["Mutex или<br/>конфайнмент в поток"]
|
||||
```
|
||||
|
||||
## Неизменяемость убирает проблему
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
data class Stats(val requests: Int = 0, val errors: Int = 0) {
|
||||
val successRate: Double
|
||||
get() = if (requests == 0) 0.0 else (requests - errors).toDouble() / requests
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val stats = MutableStateFlow(Stats())
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { i ->
|
||||
launch {
|
||||
stats.update { it.copy(requests = it.requests + 1, errors = it.errors + i % 10 / 9) }
|
||||
}
|
||||
}.forEach { it.join() }
|
||||
}
|
||||
val final = stats.value
|
||||
println("запросов: ${final.requests}, ошибок: ${final.errors}")
|
||||
println("согласованный снимок: ${final.requests == 100}")
|
||||
// запросов: 100, ошибок: 10
|
||||
// согласованный снимок: true
|
||||
}
|
||||
```
|
||||
Два связанных поля обновляются одним атомарным `copy`, поэтому несогласованного состояния («запросов 5, ошибок 7») не возникает никогда. Это главный довод за неизменяемые `data class` в качестве состояния. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
|
||||
|
||||
## Коллекции
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
fun main() = runBlocking {
|
||||
// обычный MutableList не потокобезопасен
|
||||
val unsafe = mutableListOf<Int>()
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { i -> launch { repeat(100) { unsafe.add(i) } } }.forEach { it.join() }
|
||||
}
|
||||
println("размер может быть неверным: ${unsafe.size <= 10_000}")
|
||||
|
||||
// вариант 1: Mutex
|
||||
val mutex = Mutex()
|
||||
val guarded = mutableListOf<Int>()
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { i -> launch { repeat(100) { mutex.withLock { guarded.add(i) } } } }
|
||||
.forEach { it.join() }
|
||||
}
|
||||
println("с Mutex точно: ${guarded.size == 10_000}")
|
||||
|
||||
// вариант 2: потокобезопасная коллекция из JDK
|
||||
val concurrent = ConcurrentHashMap<Int, Int>()
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { i -> launch { concurrent[i] = i } }.forEach { it.join() }
|
||||
}
|
||||
println("ConcurrentHashMap точно: ${concurrent.size == 100}")
|
||||
// размер может быть неверным: true
|
||||
// с Mutex точно: true
|
||||
// ConcurrentHashMap точно: true
|
||||
}
|
||||
```
|
||||
В Kotlin нет своих потокобезопасных коллекций для JVM - используются `java.util.concurrent`. Но учтите: `ConcurrentHashMap` безопасен для отдельных операций, а составные («прочитать, изменить, записать») всё равно требуют `compute`/`merge` или мьютекса. Подробнее: [Изменяемость](../Коллекции/Изменяемость.md)
|
||||
|
||||
## Что не нужно делать
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
fun main() = runBlocking {
|
||||
var counter = 0
|
||||
val lock = Any()
|
||||
|
||||
val time = measureTimeMillis {
|
||||
withContext(Dispatchers.Default) {
|
||||
List(100) { launch { repeat(1000) { synchronized(lock) { counter++ } } } }
|
||||
.forEach { it.join() }
|
||||
}
|
||||
}
|
||||
println("результат верный: ${counter == 100_000}")
|
||||
println("но поток блокировался: ${time >= 0}")
|
||||
// результат верный: true
|
||||
// но поток блокировался: true
|
||||
}
|
||||
```
|
||||
`synchronized` работает и даёт правильный результат, но блокирует поток диспетчера: пока корутина ждёт монитор, поток не может выполнять другие корутины. На ограниченном диспетчере это прямой путь к deadlock, а `suspend`-вызов внутри `synchronized` вообще не скомпилируется.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `synchronized` (блокирует поток) | `Mutex.withLock` (приостанавливает) |
|
||||
> | `ReentrantLock` | `Mutex` (не реентерабелен) |
|
||||
> | `Semaphore` из `j.u.c.` (блокирует) | `kotlinx.coroutines.sync.Semaphore` |
|
||||
> | `AtomicInteger` | он же, или `MutableStateFlow` |
|
||||
> | `volatile` | `@Volatile` или `StateFlow` |
|
||||
> | `ThreadLocal` для конфайнмента | элемент контекста корутины |
|
||||
> | `ConcurrentHashMap` | он же |
|
||||
> | нет аналога | актор на `Channel` |
|
||||
>
|
||||
> **`synchronized` занимает поток, `Mutex` - нет.** Это принципиальное различие. В пуле из 8 потоков восемь корутин, ждущих монитор, парализуют весь диспетчер - включая корутины, которым этот монитор не нужен. `Mutex` приостанавливает корутину и освобождает поток:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.sync.Mutex
|
||||
> import kotlinx.coroutines.sync.withLock
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val mutex = Mutex()
|
||||
> val single = Dispatchers.Default.limitedParallelism(1)
|
||||
> var progress = ""
|
||||
> withContext(single) {
|
||||
> launch { mutex.withLock { delay(30); progress += "A" } }
|
||||
> launch { mutex.withLock { progress += "B" } }
|
||||
> launch { progress += "C" } // не ждёт мьютекс
|
||||
> }
|
||||
> println("порядок выполнения: $progress")
|
||||
> // порядок выполнения: CAB
|
||||
> }
|
||||
> ```
|
||||
> На одном потоке корутина `C` выполнилась первой, пока `A` держала мьютекс и ждала на `delay`. С `synchronized` и `Thread.sleep` поток был бы занят и `C` стояла бы в очереди.
|
||||
>
|
||||
> **`suspend` внутри `synchronized` не компилируется.** Это не ограничение, а защита: приостановка освободила бы поток, а монитор остался бы захваченным - потенциальный deadlock, который JVM не заметит. `Mutex` снимает запрет, потому что владение не привязано к потоку.
|
||||
>
|
||||
> **`ReentrantLock` реентерабелен, `Mutex` - нет.** В Java повторный захват тем же потоком разрешён, что делает рекурсивные вызовы под локом безопасными. В корутинах понятие «тот же владелец» не определено: корутина мигрирует между потоками, а `withLock` в дочерней корутине - это уже другой владелец. Поэтому `Mutex` принципиально не реентерабелен, и рекурсию под локом приходится исключать архитектурно.
|
||||
>
|
||||
> **`volatile` против `StateFlow`.** `@Volatile` в Kotlin есть и работает так же: гарантирует видимость записи между потоками, но не атомарность составных операций. `MutableStateFlow` даёт и видимость, и атомарный `update`, и оповещение - три вещи вместо одной.
|
||||
>
|
||||
> **Актор не имеет аналога в Java.** Паттерн «состояние внутри потока, снаружи только сообщения» в Java требует `BlockingQueue` + выделенный поток + «отравленную пилюлю» для остановки + ручную реализацию ответов (обычно через `CompletableFuture` в сообщении). В Kotlin это `Channel` + `launch` + `CompletableDeferred` - примерно двадцать строк и никаких потоков.
|
||||
>
|
||||
> **Проблемы те же.** Корутины не отменяют ни гонок, ни deadlock'ов, ни инверсии приоритетов. Два мьютекса, захваченные в разном порядке, дают взаимную блокировку точно так же, как два `synchronized`:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.sync.Mutex
|
||||
> import kotlinx.coroutines.sync.withLock
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val a = Mutex()
|
||||
> val b = Mutex()
|
||||
> val result = withTimeoutOrNull(100) {
|
||||
> coroutineScope {
|
||||
> launch { a.withLock { delay(20); b.withLock { } } }
|
||||
> launch { b.withLock { delay(20); a.withLock { } } }
|
||||
> }
|
||||
> }
|
||||
> println("классический deadlock: ${result == null}") // классический deadlock: true
|
||||
> coroutineContext.cancelChildren()
|
||||
> }
|
||||
> ```
|
||||
> Разница только в том, что корутинный deadlock можно разорвать таймаутом, а поток, стоящий на мониторе, прервать нельзя.
|
||||
>
|
||||
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`Mutex` не реентерабелен - повторный захват это deadlock:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.sync.Mutex
|
||||
> import kotlinx.coroutines.sync.withLock
|
||||
>
|
||||
> class Service {
|
||||
> private val mutex = Mutex()
|
||||
> suspend fun outer() = mutex.withLock { inner() }
|
||||
> suspend fun inner() = mutex.withLock { "никогда" } // тот же мьютекс
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val result = withTimeoutOrNull(100) { Service().outer() }
|
||||
> println("повисло: ${result == null}") // повисло: true
|
||||
> }
|
||||
> ```
|
||||
> Публичный метод, берущий лок, не должен вызывать другой публичный метод того же класса. Выносите логику в приватные методы без блокировки.
|
||||
>
|
||||
> **`synchronized` в корутине блокирует поток диспетчера.** Результат будет верным, но пропускная способность упадёт, а на `limitedParallelism(1)` возможен deadlock. Используйте `Mutex`.
|
||||
>
|
||||
> **`suspend` внутри `synchronized` не компилируется** - и это хорошо. Попытка обойти запрет через `runBlocking` внутри `synchronized` создаёт именно тот deadlock, от которого защищает компилятор.
|
||||
>
|
||||
> **Отмена внутри `withLock` освобождает лок, но работа может остаться незавершённой:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlinx.coroutines.sync.Mutex
|
||||
> import kotlinx.coroutines.sync.withLock
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val mutex = Mutex()
|
||||
> var step = 0
|
||||
> val job = launch {
|
||||
> mutex.withLock {
|
||||
> step = 1
|
||||
> delay(1000) // здесь отмена
|
||||
> step = 2 // не выполнится
|
||||
> }
|
||||
> }
|
||||
> delay(20)
|
||||
> job.cancelAndJoin()
|
||||
> println("лок свободен: ${!mutex.isLocked}, состояние на середине: ${step == 1}")
|
||||
> // лок свободен: true, состояние на середине: true
|
||||
> }
|
||||
> ```
|
||||
> Критическая секция прервалась на середине, оставив несогласованное состояние. Либо делайте секцию атомарной (одна операция `copy`), либо восстанавливайте инвариант в `finally`.
|
||||
>
|
||||
> **`AtomicInteger` защищает одну переменную, а не инвариант:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import java.util.concurrent.atomic.AtomicInteger
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val total = AtomicInteger(0)
|
||||
> val errors = AtomicInteger(0)
|
||||
> withContext(Dispatchers.Default) {
|
||||
> List(100) { launch { total.incrementAndGet(); errors.incrementAndGet() } }
|
||||
> .forEach { it.join() }
|
||||
> }
|
||||
> println("каждый атомарен: ${total.get()} ${errors.get()}")
|
||||
> // каждый атомарен: 100 100
|
||||
> }
|
||||
> ```
|
||||
> Итог верный, но в любой момент между двумя инкрементами снимок несогласован: читатель может увидеть `total = 50, errors = 49`. Для связанных значений нужен один объект состояния.
|
||||
>
|
||||
> **`ConcurrentHashMap` не делает составные операции атомарными.** `if (!map.containsKey(k)) map[k] = v` - гонка. Нужны `computeIfAbsent`, `putIfAbsent` или `merge`.
|
||||
>
|
||||
> **Обычный `mutableListOf` в корутинах небезопасен**, даже если кажется, что обращения редки. `ArrayList.add` при конкурентном вызове может потерять элемент или бросить `ArrayIndexOutOfBoundsException` из-за состояния расширения массива.
|
||||
>
|
||||
> **Мелкозернистый конфайнмент убивает производительность.** `withContext(single) { counter++ }` внутри цикла - это переключение контекста на каждую операцию. Выносите весь блок работы в один `withContext`.
|
||||
>
|
||||
> **Два мьютекса в разном порядке - deadlock**, как и в Java. Правило то же: фиксированный порядок захвата или единый лок.
|
||||
>
|
||||
> **`Semaphore` с утечкой разрешений исчерпывается.** Ручная пара `acquire()`/`release()` без `try/finally` при исключении потеряет разрешение навсегда. Используйте `withPermit`.
|
||||
>
|
||||
> **`limitedParallelism(1)` - не замена `Mutex`.** Он гарантирует один поток для **своих** задач, но состояние может меняться и из других контекстов. Конфайнмент работает только если **все** обращения идут через этот диспетчер.
|
||||
>
|
||||
> **`@Volatile` не делает `++` атомарным.** Он даёт только видимость записи. `@Volatile var counter = 0` с `counter++` из нескольких корутин теряет инкременты точно так же, как обычное поле.
|
||||
>
|
||||
> **Актор сериализует все операции.** Если команд много, одна корутина станет узким местом - её очередь будет расти. Для высокой нагрузки нужны шардирование по ключу или атомарные типы.
|
||||
|
||||
Далее: [Корутины против потоков](Корутины%20против%20потоков.md)
|
||||
@@ -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