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)
|
||||
Reference in New Issue
Block a user