1) 概念对照:cancel vs close
| 操作 | 作用对象 | 行为 | 队列里已有元素 | 典型用途 |
|---|---|---|---|---|
| close(cause: Throwable? = null) | Channel 本身(发送端关闭) | 标记不再接收新元素,进入“关闭中” | 仍可被接收,读空后接收端结束 | 优雅收尾(读干净再停) |
| cancel(cause: CancellationException? = null) | Channel/ReceiveChannel | 立即终止,取消挂起的 send/receive | 可能丢弃未读元素 | 紧急中止(快速失败/超时/退出) |
经验法则:正常结束用 close() ,异常/超时/中止用 cancel() 。
2) 关闭/取消后的 API 行为与异常
2.1 发送侧
-
send(e):
- 通道未满→正常;
- 已关闭→抛 ClosedSendChannelException(不会返回你传给 close(cause) 的 cause)。
-
trySend(e):返回 ChannelResult:
-
成功:isSuccess = true;
-
关闭:isClosed = true,可用 exceptionOrNull() 取到 close 时的 cause;
-
满(且策略是 SUSPEND) :isSuccess = false(但不是 closed)。
-
2.2 接收侧
-
receive():
- 队列非空→取出元素;
- 关闭且已读空→抛 ClosedReceiveChannelException(不带 cause)。
-
tryReceive() / receiveCatching():
-
tryReceive() 失败时返回 ChannelResult.Failed(队列空/未关闭)或 Closed(cause);
-
receiveCatching()(挂起式安全版)返回 Success(value) 或 Closed(cause),不抛异常。
-
安全收尾推荐:for (e in ch) 或 while(true){ when(val r = ch.receiveCatching()) { … } }。
3) 取消/异常传播(结构化并发视角)
- 消费者协程失败(抛异常/被取消):不会自动关闭 Channel,但该协程停止接收;若这是唯一消费者,应主动 close/cancel Channel 或取消上游。
- 生产者协程失败:只影响该生产者;Channel 仍可被其他生产者写入。若需要“任何生产者失败即停”,请在父作用域用 非 SupervisorJob,或在失败处主动 cancel(channel)。
- 子协程:channelFlow/callbackFlow 内部子协程的异常会让构建器失败;想“一个子失败不致命”,用 supervisorScope 或子协程里 try/catch。
4) 正确的关停顺序(单/多生产者 & 管线)
4.1 单生产者 → 单消费者
- 生产者生产完 close();
- 消费者 for (e in ch) 自然读空退出;
- join 双方协程。
val ch = Channel<Item>(64)
val producer = launch {
try { produceAll().forEach { ch.send(it) } }
finally { ch.close() } // 优雅关停
}
val consumer = launch {
for (e in ch) handle(e) // 读到 close 后自动退出
}
joinAll(producer, consumer)
4.2 多生产者(fan-in)
- 不要让每个生产者各自 close() 同一个通道(会早关/竞态);
- 让协调者在 joinAll(producers) 之后统一 close() 。
val inbox = Channel<Event>(256)
val producers = listOf(srcA(), srcB(), srcC()).map { src ->
launch { src.collect { inbox.trySend(it) } }
}
val sink = launch { for (e in inbox) handle(e) }
launch { producers.joinAll(); inbox.close() } // 统一关闭
4.3 管线(pipeline)
- 上游 close → 中游读完再 close → 下游读完,像多米诺一样逐段关停。
- 紧急停止用 cancel() 从下游或根作用域一键取消所有段。
5) 资源管理:未投递元素 & 清理
5.1 onUndeliveredElement(强烈推荐)
当元素已入队但最终未被接收(例如消费者取消/通道取消)时触发,用于释放资源(文件句柄、Bitmap、ByteBuffer 等)。
val ch = Channel<ByteBuffer>(
capacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
onUndeliveredElement = { buf ->
try { buf.clear() /* 或 buf.close() */ } catch (_: Throwable) {}
}
)
该回调在渠道取消/关闭导致的未送达时被调用;正常消费不会触发。
5.2finally / NonCancellable做“扫尾”
- 在消费者 for (e in ch) 之后,用 finally 做本地清理;
- 需要阻断取消完成清理时,用 withContext(NonCancellable) 包住。
val consumer = launch {
try {
for (e in ch) process(e)
} finally {
withContext(NonCancellable) { flushToDisk() }
}
}
5.3 callbackFlow的awaitClose { … }
适配回调到 Flow/Channel 时,务必在 awaitClose 里注销监听、关闭句柄:
fun locationFlow() = callbackFlow<Location> {
val l = registerListener { trySend(it) }
awaitClose { unregister(l) } // 取消/关闭时释放
}
6) 超时与降级(与关闭/取消配合)
- 发送限时:withTimeout(300) { ch.send(x) };满缓冲且策略 SUSPEND 时超时→放弃发送。
- 接收限时:withTimeoutOrNull(500) { ch.receive() } ?: useStale()。
- select + onTimeout:更灵活,不破坏外层协程上下文。
7) 实用模板
7.1 安全接收(不抛异常,带日志)
suspend fun <T> drain(ch: ReceiveChannel<T>, handle: suspend (T) -> Unit) {
while (true) {
when (val r = ch.receiveCatching()) {
is ChannelResult.Success -> handle(r.getOrNull()!!)
is ChannelResult.Closed -> {
r.exceptionOrNull()?.let { log("closed with cause", it) }
return
}
}
}
}
7.2 “先非阻塞,失败再挂起”发送(兼容 SUSPEND 策略)
suspend fun <E> SendChannel<E>.offerOrSend(e: E) {
if (!trySend(e).isSuccess) send(e)
}
7.3 超时发送 + 业务降级
suspend fun <E> SendChannel<E>.sendOrDrop(e: E, timeoutMs: Long = 100): Boolean =
try {
withTimeout(timeoutMs) { send(e); true }
} catch (_: TimeoutCancellationException) { false }
7.4 统一关停(多生产者 + 单消费者)
suspend fun shutdown(inbox: Channel<Event>, producers: List<Job>, consumer: Job) {
producers.joinAll() // 等生产结束
inbox.close() // 让消费者读干净退出
consumer.join()
}
8) 易坑清单
- 把取消异常当普通异常吞掉:withTimeout 抛的是 TimeoutCancellationException(CancellationException 的子类)。若 catch (Exception),请对 CancellationException 直接 throw 继续传播,避免上层无法取消。
- 多个生产者各自 close:竞态早关;应由协调者统一 close。
- 只 cancel 不 close 导致消费者 for (e in ch) 不退出(仍在等)。正常结束请 close()。
- 无 onUndeliveredElement:高频/可丢场景(DROP_OLDEST/CONFLATED)容易资源泄漏。
- 在 finally 里 send:通道可能已关闭→ClosedSendChannelException。若必须发送,包 runCatching { send(...) } 或改为状态标记。
- 使用 UNLIMITED 掩盖压力:看似稳定,实则把背压变内存膨胀→高延迟/ OOM。
- 忽略多消费者公平性:Channel 是单播且不保证公平;需要均衡请路由(Round-Robin)或每消费者独立通道。
一句话总结
正常收尾 close()、异常/超时 cancel();接收端用 for/receiveCatching() 安全退出;用 onUndeliveredElement 和 finally/NonCancellable 做好资源清理;关停顺序“上游先停→逐段 close→读干净退出”;多生产者由协调者统一 close。****
把这些规则落进你的管线/工作池里,通道就既稳又不泄漏。