详解Channel核心类型:Channel<T>, SendChannel<T>, ReceiveChannel<T>

6 阅读4分钟

1. 三类接口的职责与关系

类型职责方向典型场景
SendChannel只负责发送send(T) ➜ 通道把“写端”暴露给调用方(不让它读)
ReceiveChannel只负责接收通道 ➜ receive()把“读端”暴露给调用方(不让它写)
Channel读写二合一双向在内部或同域同时读写
  • 继承关系:Channel : SendChannel, ReceiveChannel。

  • 设计目的:解耦方向。很多 API 只需要“写端”或“读端”,暴露窄接口更安全(防误用)。

2. 类型变型(非常重要)

  • SendChannel 逆变:需要发送 String 时,你可以传入 SendChannel 当作参数(Any 能接收 String)。

结论:SendChannel 是 SendChannel 的子类型。

  • ReceiveChannel 协变:有一个 ReceiveChannel,可当成 ReceiveChannel(读出来的 String 也是 Any)。

结论:ReceiveChannel 是 ReceiveChannel 的子类型。

这让“只写端/只读端”的 类型安全复用 非常自然。

3. 常用 API 速览(现代写法)

发送侧(SendChannel)

  • 挂起发送:send(value) — 缓冲满/无接收者时挂起,直到可写。

  • 非挂起尝试:trySend(value): ChannelResult

    • .isSuccess / .isFailure;失败可能是已关闭
    • 阻塞场景(非协程线程)可用 trySendBlocking(value).
  • 状态:isClosedForSend。

  • 结束:close(cause: Throwable? = null)(优雅关闭写端)。

接收侧(ReceiveChannel)

  • 挂起接收:receive(): T(通道已关且空时抛 ClosedReceiveChannelException)。

  • 非挂起尝试:tryReceive(): ChannelResult。

  • 推荐安全接收:receiveCatching(): ChannelResult

    • 成功:.getOrNull();已关闭:.isClosed,.exceptionOrNull() 取关闭原因。
  • 遍历:for (e in channel) 会在“读尽且关闭”时自然结束。

  • 状态:isClosedForReceive(注意:只有在读完缓冲后才会变 true)。

旧版 offer/poll/consumeEach 等已过时或不推荐,用上面三兄弟即可。

4. 缓冲与背压(Channel(capacity))

  • RENDEZVOUS(容量 0):严格背压,send 必须等有 receive。

  • BUFFERED(默认缓冲,大小依实现/平台而定,通常 64):适度吸收生产突刺。

  • CONFLATED:只保留最新元素(状态类消息/热信号,很像 StateFlow)。

  • UNLIMITED:理论无限队列,小心 OOM

    背压策略直接决定 send/receive 的挂起时机,影响吞吐与内存占用。

5. 生命周期:close vs cancel

  • close(cause?)(发送侧调用):

    • 阻止后续发送;保留/允许把缓冲区剩余元素读完;接收侧用 receiveCatching() 能看到关闭并读尽。
  • cancel(cause?)(任意一侧):

    • 立即失败并丢弃未读元素,所有挂起操作以 CancellationException 结束。
  • 经验法则:谁负责生产,谁负责 close;消费者若不想再读,用 cancel() 终止消费并释放资源。

6. 所有权与结构化并发(最佳实践)

  • 通道应作为“协程之间”的单向资源

    1)生产者创建 Channel;

    2)把 只读端 ReceiveChannel 交给消费者;

    3)生产结束时 生产者 close

    4)消费者用 for (e in ch) 或 receiveCatching() 自然退出。

  • 放到 coroutineScope {} 中,确保异常能级联取消,避免泄漏。

7. 典型用法示例

7.1 一生产一消费(优雅关闭)

val ch = Channel<Int>(capacity = Channel.BUFFERED)

val producer = launch {
    try {
        repeat(100) { i ->
            ch.send(i)  // 缓冲满则挂起
        }
    } finally {
        ch.close()    // 生产者负责 close
    }
}

val consumer = launch {
    for (x in ch) {   // 读尽+关闭 => 正常退出
        println("got $x")
    }
}

7.2 多生产者聚合(fan-in)

val ch = Channel<String>(Channel.BUFFERED)

val workers = List(3) { idx ->
    launch {
        repeat(5) { j ->
            ch.trySend("[$idx] $j").onFailure {
                // 满/关闭时可做降级或重试
            }
        }
    }
}

val sink = launch {
    for (msg in ch) println(msg)
}

// 等待所有生产者结束后关闭写端
launch {
    workers.joinAll()
    ch.close()
}

7.3 多消费者分发(fan-out)

val ch = Channel<Job>(Channel.BUFFERED)

// 3 个消费者“抢任务”
repeat(3) { c ->
    launch {
        for (task in ch) {
            task.run()
        }
    }
}

// 生产若干任务
launch {
    repeat(100) { i -> ch.send(Job(i)) }
    ch.close()
}

7.4 与超时/合并事件(select简述)

select {
    ch.onReceiveCatching { res ->
        if (res.isSuccess) handle(res.getOrNull()!!)
        else println("closed")
    }
    onTimeout(200) {
        println("tick")
    }
}

select 适合“多个通道/事件就绪哪个先来”的竞态合并。

8. 与 Flow 的关系(只需一个心智模型)

  • ReceiveChannel.consumeAsFlow() 可以转成 Flow;

  • 状态/背压:Flow 更适合“声明式、冷流、链式变换”;Channel 是“热流/总线、主动推送、需要手动管理生命周期与背压**”的工具。很多时候用 Flow/SharedFlow/StateFlow 就足够,只有当你需要“协程之间点对点消息 + 明确背压/容量控制”时,才选 Channel。

9. 常见坑 & 排错清单

  1. 卡死/不出数:RENDEZVOUS 下没有接收者时,send 会挂起;或缓冲已满。用 trySend 观察结果,或加日志看是否卡在 send/receive。
  2. 关闭读取不到:只用 receive() 会在“关且空”时抛异常;更推荐 receiveCatching() 或 for (e in ch) 循环。
  3. 谁来 close:始终由生产者 close;消费者要中断消费,用 cancel()。
  4. 无限缓冲导致内存涨:避免 UNLIMITED;用 BUFFERED/CONFLATED 或把生产速率控住。
  5. 混用多个代理(网络问题类比) :在 app 里混用多条通道作为总线容易乱,明确边界:一个职责一条通道,读写端尽量只暴露需要的接口。
  6. 异常传播:协程取消会让通道操作抛 CancellationException,注意 try/finally 释放资源。