Channel取消、关闭、异常与资源管理

71 阅读4分钟

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 单生产者 → 单消费者

  1. 生产者生产完 close();
  2. 消费者 for (e in ch) 自然读空退出;
  3. 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) 易坑清单

  1. 把取消异常当普通异常吞掉:withTimeout 抛的是 TimeoutCancellationException(CancellationException 的子类)。若 catch (Exception),请对 CancellationException 直接 throw 继续传播,避免上层无法取消
  2. 多个生产者各自 close:竞态早关;应由协调者统一 close
  3. 只 cancel 不 close 导致消费者 for (e in ch) 不退出(仍在等)。正常结束请 close()。
  4. 无 onUndeliveredElement:高频/可丢场景(DROP_OLDEST/CONFLATED)容易资源泄漏
  5. 在 finally 里 send:通道可能已关闭→ClosedSendChannelException。若必须发送,包 runCatching { send(...) } 或改为状态标记。
  6. 使用 UNLIMITED 掩盖压力:看似稳定,实则把背压变内存膨胀→高延迟/ OOM
  7. 忽略多消费者公平性:Channel 是单播且不保证公平;需要均衡请路由(Round-Robin)或每消费者独立通道。

一句话总结

正常收尾 close()、异常/超时 cancel();接收端用 for/receiveCatching() 安全退出;用 onUndeliveredElement 和 finally/NonCancellable 做好资源清理;关停顺序“上游先停→逐段 close→读干净退出”;多生产者由协调者统一 close。****

把这些规则落进你的管线/工作池里,通道就既稳又不泄漏。