546 lines
26 KiB
Markdown
546 lines
26 KiB
Markdown
---
|
||
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)
|