Kotlin之Channel

469 阅读21分钟

ChannelFlow 简介与对比

1. 基本概念

名称类型特性典型场景
Flow冷流- 每次有下游(collector)订阅才启动
- “一对一”流,每个订阅者独立执行上游逻辑
简单的一次性数据流,例如从网络依次拉取多页数据
SharedFlow热流- 由上游持续发射,不依赖是否有订阅者
- 多播,所有订阅者共享同一条数据流
- 可配置 replay、buffer
广播事件、消息总线;需要多个观察者同时接收事件
StateFlow热流,SharedFlow 的子接口- 始终有一个初始值,且只保留最新一个值
- 订阅时会立刻收到当前最新状态
- 值是“对等替换”性质(conflated)
UI 状态持有与订阅,如“当前用户登录状态”、“界面主题色”
Channel线程/协程间通信原语- 发送(send)和接收(receive)
- 支持多种缓冲策略(Rendezvous、Buffered、Conflated、Unlimited)
- 一旦接收就从缓冲区移除
协程间的点对点消息传递、生产者—消费者模型
Flow<T>              —— 冷流接口
  ↑
  └── SharedFlow<T>  —— 热流接口(可 emit、可 replay)
        ↑
        └── StateFlow<T>  —— 热流子接口(有初始值、只保留最新值)

2. 详细对比

2.1 启动时机

  • Flow:冷流;只有调用 flow { … } 并且有下游 .collect { … } 时才会执行上游代码。
  • SharedFlow/StateFlow:热流;上游通过 emit() 推送时,先于订阅者存在。

2.2 多订阅者行为

  • Flow:多订阅者时,每个订阅者都会重新执行整个上游逻辑,互不干扰。
  • SharedFlow/StateFlow:多订阅者共享同一数据源,上游只执行一次。

2.3 缓存与重放(Replay)

  • Flow:无重放概念,下游错过就错过。
  • SharedFlow:可通过构造参数 replay = n 设置缓存最近 n 条,下游订阅时会收到这些历史事件。
  • StateFlow:内置 replay = 1,且必须有初始值;下游订阅时立刻收到最后一次状态。

2.4 最新值语义(Conflation)

  • StateFlow:只保留最新值;当上游多次 emit() 而下游处理慢时,未消费的中间值会被合并丢弃,只保留最新那次。
  • SharedFlow:若 buffer 设置为 conflated,也有类似效果;否则按缓冲区顺序消费。
  • Channel:不同策略(比如 Rendezvous、Buffered、Conflated)决定是否保留或丢弃历史消息。

2.5 典型使用建议

  • StateFlow

    • 状态管理:UI 状态、单例数据缓存、ViewModel 与 UI 绑定
    • 订阅者随时订阅都能拿到“当前状态”
  • SharedFlow

    • 事件总线:全局广播、一次性事件(toast、导航指令等)
    • 需要“历史事件”可配置 replay;不需要可设为 replay=0
  • Flow

    • 一步步生成流:分页加载、组合多个异步请求、文件读取
  • Channel

    • 协程协作:生产者—消费者模式、需精细控制缓冲和 back-pressure
    • 不用于“状态订阅”,也不建议跨线程做 UI 更新

3. 几个需要注意的地方

  1. StateFlow 是继承自 SharedFlow 的一个子接口,专门用来做“状态持有者”。

  2. SharedFlow 继承Flow,并在其基础上增加了发射(emit)和重放(replay)等能力。

  3. Channel 不仅仅是多路版的 async

    • Channel 更底层,提供了多种 buffer 策略,可直接进行 send/receive;而 async 只是开启一个协程并返回 Deferred。
  4. 性能与场景

    • 若只做“跨线程”一次性数据传递,可直接用 FlowflowOn 切换调度);若需要细粒度的 back-pressure 或挂起特性,可考虑 Channel。

SendChannelReceiveChannel 的定义、核心 API,以及二者的对比

Channel的父类包含SendChannelReceiveChannel, 拆分成2个接口是为了api 的纯净。


一、定义

  • SendChannel<E>

    • 表示通道的“发送端”接口,只暴露发送相关的能力。
    • 继承自 Channel<E>,但通常只当作只能发不收的引用使用。
  • ReceiveChannel<E>

    • 表示通道的“接收端”接口,只暴露接收相关的能力。
    • 同样继承自 Channel<E>,但只当作只能收不发的引用使用。

二、核心 API

1. SendChannel 主要成员

  • send(element: E):挂起直到发送成功或通道关闭,抛出异常。
  • trySend(element: E):立即返回 ChannelResult,不挂起。
  • close(cause?):关闭发送端,通知所有接收方没有更多元素。
  • isClosedForSend:检查发送端是否已关闭。
  • invokeOnClose { cause -> … }:注册关闭时的回调。

2. ReceiveChannel 主要成员

  • receive():挂起直到有元素或通道关闭,抛出关闭异常后退出。
  • receiveCatching():挂起并返回 ChannelResult,不会抛异常。
  • tryReceive():立即返回 ChannelResult,不挂起。
  • isClosedForReceive:检查接收端是否已关闭。
  • cancel(cause?):取消接收端(并关闭底层通道)。

三、对比与职责分离

  1. 职责分离

    • 将一个完整的 Channel<E> 拆分成“能发”和“能收”两部分,分别由生产者和消费者持有,利于职责清晰、类型安全。
  2. 阻塞与否

    • send/receive 会挂起;trySend/tryReceive 不挂起,立即返回结果。
  3. 异常处理

    • 挂起版在失败时直接抛异常;尝试版则用 ChannelResult 把状态与异常封装起来,由调用方自行检查。
  4. 关闭行为

    • SendChannel 可主动调用 close()ReceiveChannel 则通过 cancel() 或在读完所有元素后自然完成。

四、示例

// 1. 创建一个双端可用的通道
val channel = Channel<Int>(capacity = 5)

// 2. 生产者(持有 SendChannel)
val producer = launch {
  for (i in 1..10) {
    channel.send(i)                    // 挂起发送
    println("Sent $i")
  }
  channel.close()                      // 发送完毕后关闭
}

// 3. 消费者(持有 ReceiveChannel)
val consumer = launch {
  for (x in channel) {                 // 自动循环 receive(),直到通道关闭
    println("Received $x")
  }
  println("Channel is closed, consumer exits")
}
  • 如果只想暴露发送能力,可以写成:

    val sender: SendChannel<Int> = channel
    
  • 如果只想暴露接收能力,可以写成:

    val receiver: ReceiveChannel<Int> = channel
    

produce()来提供跨协程的事件流

1. 使用场景
当我们需要对同一个接口做持续多次的轮询请求(比如实时更新数据)时,async 只会执行一次任务、返回一次结果,不太适合;而 produce 或者基于 Channel 的方案可以不断地产生并推送新数据。


2. async 特性

val scope = CoroutineScope(EmptyCoroutineContext)
val result1 = scope.async { gitHub.contributors("A","B") }
val result2 = scope.async { gitHub.contributors("C","D") }
println(result1.await() + result2.await())
  • async { … } 启动一个新协程,返回一个 Deferred<T>
  • await() 只是“暂停”当前协程直到结果就绪,不会阻塞底层线程。
  • 结果产生后,再次调用同一个 Deferredawait(),返回值是相同的——它只执行了一次。

在需要持续轮询服务器以保持数据更新的场景下,虽然常见的方案有 HTTP 轮询、WebSocket 或 SSE(Server-Sent Events),但 async 由于只会产生一次性结果,并不适合长期运行。更好的做法是:

  1. 使用 FlowSharedFlow,它们可以在后台不断发射新数据;
  2. 或者用 Channelproduce { … } + receive 模式,将数据生成和发送放在一个协程里,不断调用 send(),在另一个协程里循环 receive(),从而实现持续刷新的效果。

3. produce 特性

// 1. 生产者协程,不断 send 数据
val receiver: ReceiveChannel<List<Contributor>> = scope.produce {
  while (isActive) {
    val data = gitHub.contributors("square", "retrofit")
    send(data)      // 推送新数据
  }
}
// 2. 消费者协程,定时 receive
scope.launch {
  delay(5000)
  while (isActive) {
    println("Contributors: ${receiver.receive()}")
  }
}
  • produce { … } 启动一个带有 ProducerScope 的协程,返回一个 ReceiveChannel<T>
  • produce 的代码块内,可以多次调用 send(value),不断“生产”数据。
  • 外部协程通过 receive() 一次次地拿到每次 send 推送的不同数据。

4. async vs. produce 对比

特性async / Deferredproduce / ReceiveChannel
执行次数代码块只执行一次可以在循环中多次执行(send 多次)
返回类型Deferred<T>,一次性拿到结果ReceiveChannel<T>,多次 receive
获取数据await() 多次调用结果相同receive() 每次调用拿到下一个数据
典型用途并发请求一次性结果实时或周期性数据推送

总结

  • async 看作“单次生产”的异步任务,它返回一个只能完成一次计算的 Deferred
  • produce(底层基于 Channel)看作“多次生产”的异步数据流,它通过 send/receive 构建了一个可反复获取新数据的事件流模型。

这样,使用 produce + receive 就能在一个协程内部持续生产数据,其他协程通过 ReceiveChannel 不断取用,非常适合轮询或实时更新的场景。

asyncproduce 的本质区别在于

  • async 返回一个只能完成一次计算的 Deferred<T>;在大括号内只执行一次异步任务,后续再调用 await() 只能读取同一个结果。要想拿到新数据,就必须“重新”调用 async { … } 并再次 await()
  • produce 返回一个支持多次 “生产—消费” 循环的 ReceiveChannel<T>;在大括号内可以反复调用 send(value) 推送新数据,外部通过 receive() 一次次取出每次不同的值。

换句话说,把 produce 看作“多条数据版的 async”

  • 它在生产者协程里持续运行,通过 send 发送多条数据;
  • 消费者协程调用 receive,每次都能接收到最新一次 send 推送的内容。

这样,produce + receive 就天然构建起一个跨协程的事件流模型,非常适合用来轮询或实时推送场景。

actor():把 SendChannel 暴露出来

1. 通道与协程的基础模式

// 1) 手动创建 Channel,再分别用两个协程进行发送和接收
val channel = Channel<List<Contributor>>()

// 发送端
scope.launch {
  while (isActive) {
    val data = gitHub.contributors("square", "retrofit")
    channel.send(data)
  }
}

// 接收端
scope.launch {
  delay(5_000)
  while (isActive) {
    println("Contributors: ${channel.receive()}")
  }
}

这样可以在不同协程间传递数据,但需要手动管理通道的创建、发送和接收。


2. produce { … }:通道 + 发送 合一

  • 返回值ReceiveChannel<T>
  • 内部上下文ProducerScope<T>,可以调用 send(value)
val receiver: ReceiveChannel<List<Contributor>> = scope.produce {
  while (isActive) {
    val data = gitHub.contributors("square", "retrofit")
    send(data)
  }
}

scope.launch {
  delay(5_000)
  for (contributors in receiver) {
    println("Contributors: $contributors")
  }
}
  • 特点

    • 通道的创建和数据发送写在同一个协程体内。
    • 对外只暴露只读的 ReceiveChannel,上游协程只能从中 receive

3. actor { … }:通道 + 接收 合一

  • 返回值SendChannel<E>
  • 内部上下文ActorScope<E>,可以通过 for (msg in channel) 或者 channel.receive() 来读取消息
val scope = CoroutineScope(EmptyCoroutineContext)

// actor 本质上创建了一个 Channel<Int>,并启动一个协程来消费它
val sender: SendChannel<Int> = scope.actor<Int> {
  // `channel` 是内部创建的 ReceiveChannel<Int>
  for (num in channel) {
    println("Number: $num")
  }
}

scope.launch {
  for (n in 1..100) {
    sender.send(n)    // 向 actor 的通道发送
    delay(1_000)
  }
}
  • 特点

    • 通道的创建和数据消费写在同一个协程体内。
    • 对外只暴露写入接口 SendChannel,下游只能 send,而内部协程负责 receive

4. 对比小结

特性produce { … }actor { … }
返回类型ReceiveChannel<T>SendChannel<T>
内部上下文ProducerScope<T>ActorScope<E>
主要用途生产者模式:生成并发送数据消费者模式:接收并处理数据
暴露给调用方接收端(只能 receive发送端(只能 send
典型写法send(...)for (msg in channel)receive()

通过上面的整理,可以清晰地看到:

  • produce 是“通道+发送”的快捷方式,用于封装“数据生产”逻辑;
  • actor 是“通道+接收”的快捷方式,用于封装“数据消费”逻辑。

在实际使用中,根据场景选择:

  • 如果你更关心“如何生成并推送数据”,用 produce
  • 如果你更关心“如何接收并处理外部消息”,用 actor

Channel的工作模式详解

// ProduceScope,它的父类是 SendChannel,拥有 send 方法,实现了数据的持续传递与发送。
val receiver = scope.produce { // 这里的 this = ProducerScope,它的父类是 SendChannel,拥有 send 方法,实现了数据的持续传递与发送。
  while (isActive) {
    val data = gitHub.contributors("square", "retrofit")
    send(data)
  }
}

同时我们也知道,launch 函数里边的 CoroutineScopejob 对象与 launch 整个函数的返回值(Job 对象)是同一个,这点和 produce 是一样的:它的 ProducerScope 与返回值 ReceiveChannel 其实都是同一个协程对象。因此,这里调用的 send(data)receiver.receive() 操作的实际上是同一个底层通道(Channel),只是发送与接收一般运行在不同的协程中。

launch {
  delay(5000)
  while (isActive) {
    println("Contributors: ${receiver.receive()}")
  }
}

Channel 对象同时实现了发送和接收功能(ProducerCoroutine 实现了 Channel 接口),定义如下:

public interface Channel<E> : SendChannel<E>, ReceiveChannel<E>

最“原始”也最干净的 Channel 用法:

val channel = Channel<List<Contributor>>()  // 默认无缓冲
scope.launch {
  channel.send(gitHub.contributors("square", "retrofit"))
}
scope.launch {
  while (isActive) {
    val latest = channel.receive()
    println("Contributors: $latest")
  }
}

协程的 Channel 就是一种在协程之间共享数据的通道,这个通道有“入口”和“出口”:

  • 在任意协程中调用 send(...),数据进入通道;
  • 在任意协程中调用 receive(),数据从通道取出。

当在不同协程里分别进行 sendreceive 调用时,数据就能从一个协程发送到另一个协程。

produce,实质上是对上述模式的封装:

  • 它在内部创建了一个 Channel<E>
  • 启动一个 ProducerCoroutine,把 Channel 的能力委托给 ProducerScope
  • 将“创建 Channel + 开协程发送”二合一,并直接返回 ReceiveChannel<E>
internal fun <E> CoroutineScope.produce(
    context: CoroutineContext = EmptyCoroutineContext,
    capacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    onCompletion: CompletionHandler? = null,
    @BuilderInference block: suspend ProducerScope<E>.() -> Unit
): ReceiveChannel<E> {
    val channel = Channel<E>(capacity, onBufferOverflow) // 创建了Channel对象,
    val newContext = newCoroutineContext(context)
    val coroutine = ProducerCoroutine(newContext, channel)// 把Channel对象放进ProducerCoroutine,ProducerCoroutine实现了Channel接口。传入channel对象是为了做接口委托。把Channel接口功能全部都委托给了这个channel对象
    if (onCompletion != null) coroutine.invokeOnCompletion(handler = onCompletion)
    coroutine.start(start, coroutine, block)
    return coroutine
}

// Channel参数传递过来了:
private class ProducerCoroutine<E>(
    parentContext: CoroutineContext, channel: Channel<E>
) : ChannelCoroutine<E>(parentContext, channel, true, active = true), ProducerScope<E> {
......
}

internal open class ChannelCoroutine<E>(
    parentContext: CoroutineContext,
    protected val _channel: Channel<E>,
    initParentJob: Boolean,
    active: Boolean
) : AbstractCoroutine<Unit>(parentContext, initParentJob, active),
    Channel<E> by _channel // _channel就是外边传进来的。通过这种方式就把外边的Channel对象传到里边来了。所以说produce就是实现了对Channel的封装。把Channel的创建和数据的发送包在了协程的内部。
{
 ......   
    }

public interface Channel<E> : SendChannel<E>, ReceiveChannel<E> {

总结

  • 当业务逻辑分散,需要手动创建 Channel、手动开启生产者和消费者协程时,可直接用 Channel
  • 如果想把 Channel 的创建与协程生产逻辑捆绑在一起,代码结构更简洁,推荐使用 produce

Channel 的作用就是在协程之间建立一个“挂起式队列”,用来一条一条地传递数据。
它拆分为 SendChannel(发送端)和 ReceiveChannel(接收端)只是为了暴露更干净的 API,实际对象同时实现了二者。
底层数据结构类似于 BlockingQueue,不同点在于:

  1. BlockingQueue 在满或空时会阻塞线程;
  2. Channel 则“挂起”协程,不会阻塞线程。

正因为 Channel 是“点对点”消费——一条数据只能被一个接收者拿走——所以它不适合做多订阅的事件流。如果需要「广播式」订阅,应使用 SharedFlow 等更合适的 API。

Channel 只是把阻塞改为了协程的挂起。当元素满了的时候,他会把写入的协程挂起,当元素为空的时候,它会把读取的协程挂起。而不是卡住线程。Channel 本质上是一个“挂起式队列”(类似于 BlockingQueue),它同时实现了 SendChannel(发送端)和 ReceiveChannel(接收端)接口。

  • 当队列满时,send() 会挂起发送方协程;

  • 当队列空时,receive() 会挂起接收方协程。

因为每次 send 的数据只会被一个 receive 拿走,Channel 适合点对点的场景,但不支持广播式的多订阅。为什么呢?因为当我在多个协程里分别读取数据的时候,谁能获取到数据是不一定的,每条 send 的数据只会被 一个 receive 拿走,多消费者会竞争,无法保证每个消费者都能收到同一条消息。例如下边的代码:

示例:多协程竞争接收

val scope = CoroutineScope(EmptyCoroutineContext)
val channel = Channel<List<Contributor>>()

// 生产者:只 send 一次
scope.launch {
  channel.send(gitHub.contributors("square", "retrofit"))
}

// 两个竞争者同时 receive
scope.launch { channel.receive() }
scope.launch { channel.receive() }
  • 第一次 send 时,不确定是哪一个 receive 拿到数据;
  • 第二次 send(若有)交给剩下的协程,无法保证每个订阅者都收到完整序列。

示例:持续接收模式

// 两个消费者都在循环中 receive
scope.launch {
  while (isActive) {
    val data = channel.receive()
    // …处理 data…
  }
}
scope.launch {
  while (isActive) {
    val data = channel.receive()
    // …处理 data…
  }
}

依旧是“分发给某一个”而非“复制给所有人”,所以多个协程并不能各自收到完整的事件流。


正确的多订阅事件流

  • 若需要广播式、可由多个订阅者各自获取完整序列,应使用 SharedFlow(或其前身 BroadcastChannel)。
  • SharedFlow 支持配置 replay 和缓冲策略,新订阅者也能拿到最近若干条事件。

补充:Channel 在 Flow 中的角色

  • Flow 底层的 buffer()、背压等机制,就是基于 Channel 实现的缓冲队列;
  • 但纯粹的事件广播场景,请选择 SharedFlow,而非直接使用 Channel。

Channel API详解

一、挂起式遍历订阅 Channel

在前面,我们通过死循环不断 receive() 实现了“轮询式订阅”。有一种更简便的写法:对 Channel 对象直接用 for 循环遍历。

val channel = Channel<List<Contributor>>(CONFLATED)

scope.launch {
  channel.send(gitHub.contributors("square", "retrofit"))
}

launch {
// 普通写法:
//  while (isActive) {
//    val contributors = channel.receive()
//    println("Contributors: $contributors")
//  }
  
// 更简便的挂起式遍历:
  for (data in channel) {
    println("Contributors: $data")
  }
}

特点

  • 即便当前没有元素,for (element in channel) 也会将协程挂起,等待下一个 send
  • 当通道关闭且所有元素都被消费完毕后,循环才会结束。

有别于普通的集合遍历,Channel的遍历是挂起式的,即便没有元素,也会把协程挂起,来等着下一个元素的出现。

挂起式遍历:和普通集合有什么不同?

  • 普通集合遍历:一次性快速遍历,遇到空集合直接结束。

  • Channel 遍历

    • 使用 for (element in channel) 时,即便当前没有元素,也会挂起协程,等待下一个元素到来。
    • 当新元素通过 send() 进来,挂起的协程恢复,处理该元素。
    • 只要通道没有被关闭,循环就一直存在;只有在通道关闭并且所有已有元素都被消费完后,循环才结束。

因此,在 Kotlin 协程中,for (element in channel) 的遍历行为并不是像普通集合的遍历那样一次性快速完成,而是挂起式的。这种挂起式遍历行为使得遍历可以等待新的元素(消息)到达通道。在 Channel 中,元素就是通过通道发送和接收的消息。

以下是一个使用 Channel 在两个协程之间传递整数消息的示例:

fun main() = runBlocking {
    val channel = Channel<Int>()

    // 启动发送消息的协程
    val sender = launch {
        for (x in 1..5) {
            delay(1000) // 模拟一些延迟
            channel.send(x) // 发送元素(消息)到通道
            println("Sent: $x")
        }
        channel.close() // 关闭通道
        println("Channel closed")
    }

    // 启动接收消息的协程
    val receiver = launch {
        for (element in channel) {
            println("Received: $element") // 接收元素(消息)从通道
        }
        println("No more elements. Channel is closed.")
    }

    // 等待发送者和接收者协程完成,不过实际上这个代码没有必要,因为runBlocking就是会等待自己内部所有的流程执行完了才会结束自己。
    sender.join()
    receiver.join()
    println("All done")
}

在上边的代码中:

  • 发送端

    • 每秒调用 send(x),一旦发送完成就打印 Sent: x
    • 发送完毕后调用 close(),通知接收者“没有更多元素”。
  • 接收端

    • for (element in channel) 循环挂起式地等待并接收每一个元素。
    • 当通道关闭且所有元素都被取走后,循环自动结束,打印结束提示。

这样,我们可以直观地看到:Channel 的遍历会在没有数据时“挂起”,等待下一条消息,而不是像普通集合那样马上结束。

持续等待新消息

  • 如果不调用 channel.close() 来关闭通道,Channel 会一直存在,持续挂起等待新的消息到来。
  • send(value) 会把数据放入通道头部;receive() 会从通道头部取出数据。

默认缓冲行为

  • 默认情况下,Channel()capacity = 0(Rendezvous 模式),意味着没有缓冲区:

    public fun <E> Channel(
        capacity: Int = RENDEZVOUS,
        onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
        onUndeliveredElement: ((E) -> Unit)? = null
    ): Channel<E>
    
  • 在这种模式下,第一次调用 send() 就会挂起发送方协程,直到有协程调用 receive() 消费该消息。

  • 如果多个 send 连续调用而没有 receive,则队列会无限增长直至达到指定容量;超过容量后,行为取决于 onBufferOverflow 策略。


挂起函数:send 与 receive

  • sendreceive 都是 挂起函数,只有协程被挂起,不会阻塞底层线程。
  • 因此,使用 send/receive 模式时,必定有一个协程在等待对方。 当然我们也可以定制缓冲区,也就是第一个参数。
fun main() = runBlocking {
  val channel = Channel<Int>(8) // 缓冲区大小未8

  // 启动发送消息的协程
  val sender = launch {
    for (x in 1..20) {
      delay(100) // 模拟一些延迟
      channel.send(x) // 发送元素(消息)到通道
      println("Sent: $x")
    }
    // Channel.close() 不会丢掉缓冲区里已经 `send` 进去的数据,
    // 它只是告诉消费者“接下来不会再有新元素”,消费者会继续取完旧数据,然后干净地结束。
    channel.close() // 关闭通道
    println("Channel closed")
  }

  // 启动接收消息的协程
  val receiver = launch {
    for (element in channel) {
      delay(2000) // 延时2秒来处理消息,依然能够接收到20条消息,且即便Channel已经close掉了,后续的消息还是能够收到。
      println("Received: $element") // 接收元素(消息)从通道
    }
    println("No more elements. Channel is closed.")
  }

  // 等待发送者和接收者协程完成
  sender.join()
  receiver.join()
  println("All done")
}
  • 前 8 条消息
    发送者会很快地把 1 到 8 这 8 条消息放入缓冲区,不会挂起。
  • 超过缓冲区后挂起
    当第 9 条消息到来时,缓冲区已满,send(9) 就会挂起发送者,直到接收者消费至少一条消息(腾出一个槽位)。
  • 消费释放空间
    接收者每处理完一个元素(每次延时 2s),就会 receive() 一次,缓冲区便腾出一个位置。
    此时被挂起的发送者恢复,放入第 9 条;而后续发送者又可能挂起——如此循环,直到全部 20 条都被发送和接收。
  • 通道关闭后依然消费剩余缓冲
    发送者在发完 1–20 后调用 close(),但循环 for (element in channel) 会继续取出并处理缓冲区里剩下的所有消息,才真正结束。

缓冲区溢出策略
Channel 支持三种溢出处理策略:

  1. SUSPEND(默认)

    • 挂起发送方直到有缓冲空间。
  2. DROP_OLDEST

    • 丢弃最旧的一条消息,再把新消息放入队列。
  3. DROP_LATEST

    • 丢弃当前要发送的新消息,不改变队列内容。
// 等价写法:容量 1 且丢弃最老 / conflated 模式
val channel1 = Channel<List<Contributor>>(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val channel2 = Channel<List<Contributor>>(capacity = CONFLATED)

可以根据实际需求,选择合适的 缓冲大小溢出策略,同时利用 send/receive 的挂起特性,实现背压和流控。

Channel 的关闭,SendChannel#cancel 与 Receive#close

1. close():关闭发送端,不影响已提交的数据流

  • 调用时机:当生产者完成所有要发的数据后,调用 channel.close(),表示“不会再有新消息”了。

  • 对发送端的影响

    • SendChannel.isClosedForSend 会变成 true
    • 之后再调用 send(...) 会立即抛出 ClosedSendChannelException
  • 对接收端的影响

    • 仍然可以接收缓冲区里已经 send 进来的所有元素;
    • 挂起中的 receive() 会照常恢复并取出下一条数据;
    • 只有当缓冲区被取空且通道标记为关闭后,for (e in channel) 循环才会结束,或者再调用 receive() 会抛出 ClosedReceiveChannelException
  • 关键点close() 只是告诉通道 “再也不会有新元素加入”,但不丢弃已经提交的消息。

val channel = Channel<Int>(capacity = 8)
launch {
    channel.send(1)
    channel.send(2)
    channel.close()  // 之后不能再 send()
}
launch {
    for (e in channel) println(e)  // 会打印 1、2,然后循环结束
}

2. cancel():从接收端发起的“强制关闭”与清理

  • 用途:当消费者不再需要任何后续消息时,可以调用 ReceiveChannel.cancel()

  • 效果

    • 通道立即以 CancellationException 关闭;
    • 丢弃缓冲区内尚未消费的消息;
    • 所有挂起的 sendreceive 调用都会被恢复并抛出 CancellationException
    • SendChannel.isClosedForSendReceiveChannel.isClosedForReceive 都会变为 true
  • 关键点cancel() 相当于“彻底取消”,不仅禁止后续 send/receive,还会清理队列并中断所有挂起。

val channel = Channel<Int>(capacity = 8)
launch {
    for (x in 1..100) channel.send(x)  // 可能一直挂起或缓冲
}
launch {
    repeat(3) { println(channel.receive()) }  // 只处理前 3 条
    channel.cancel()  // 剩余的 97 条被丢弃,任何挂起都会抛 CancellationException
}

3. 异常类型

操作异常类型什么时候抛
在已关闭通道上 sendClosedSendChannelExceptionclose() 之后调用
在已关闭且空通道上 receiveClosedReceiveChannelException缓冲消耗完毕后再调用 receive()
cancel() 后的任意挂起调用CancellationException立即中断所有挂起的 send/receive

总结

  • close():优雅完成——不接受新消息,但把已提交的数据全部送达下游;
  • cancel():强制中断——清理所有挂起和缓冲,抛出取消异常。
// send再发送数据的异常。
public class ClosedSendChannelException(message: String?) : IllegalStateException(message)
// receive再接收数据的异常
public class ClosedReceiveChannelException(message: String?) : NoSuchElementException(message)
1. close(cause: Throwable?):支持自定义异常
  • 除了无参的 close(),你还可以传入一个 Throwable

    channel.close(MyCustomException("Stream ended unexpectedly"))
    
  • 这样,任何后续对空通道的 receive() 都会抛出一个包装了你指定 causeClosedReceiveChannelException,便于在上层捕获专属的错误语义。


2. cancel(cause: CancellationException):接收端发起的“彻底取消”
  • 当消费者不再需要后续数据时,调用 cancel()

    receiveChannel.cancel(CancellationException("No longer needed"))
    
  • 效果:

    • isClosedForSendisClosedForReceive 同时置为 true
    • 丢弃缓冲区和所有挂起的 send/receive 调用;
    • 所有被挂起的 send/receive 立即恢复并抛出 CancellationException
    • 适合“立即中断并清理”场景。

3. 未投递元素回调:onUndeliveredElement
  • 如果 Channel 中的元素承载了需要手动释放的资源(如文件、数据库连接等),直接 cancel() 可能导致这些资源未被消费、得不到关闭,从而泄漏。

  • 解决方案:利用构造函数的第三个参数 onUndeliveredElement 来处理那些因取消、缓冲溢出或关闭而无法正常被 receive 的元素:

    val channel = Channel<FileStream>(
      capacity = 0,                      // 或任意合适容量
      onBufferOverflow = BufferOverflow.SUSPEND,
      onUndeliveredElement = { file ->
        file.close()                    // 元素“丢弃”时关闭文件
      }
    )
    
  • 何时调用:当元素因以下任一情况未被正常消费时,onUndeliveredElement 都会被触发:

    1. cancel() 立即丢弃所有挂起和缓冲中的元素;
    2. 缓冲区溢出且策略为 DROP_OLDESTDROP_LATEST
    3. 通道执行 close(cause) 后,还有尚未 receive 的元素。

这样,

  • close(cause) 定制流结束的异常语义;
  • cancel() 实现“彻底中断并清理”;
  • onUndeliveredElement 回调,确保所有资源都能在元素无法正常到达消费者时得到释放。

send / receive还有一对兄弟函数:trySend / tryReceive

1. send / receive vs. trySend / tryReceive

  • sendreceive

    • 都是 挂起函数(suspending),会在以下情况下挂起协程:

      • send:当通道缓冲区已满时,挂起直到有空位或通道关闭。
      • receive:当通道为空时,挂起直到有元素或通道关闭。
    • 失败时会 抛出异常(例如 ClosedSendChannelExceptionClosedReceiveChannelException)。

  • trySendtryReceive

    • 不是挂起函数,不会阻塞线程。

    • 调用时会 立即返回一个 ChannelResult<T>

      • 如果操作成功,ChannelResult 中包裹了数据或成功标记;
      • 如果缓冲区满(对 trySend)或通道空(对 tryReceive),则返回失败结果;
      • 如果通道已关闭,失败结果中也会标记“已关闭”。

2. ChannelResult<T> 的常用 API

public sealed class ChannelResult<out T> {
    // true 如果 holder 是 Closed 或者其他 Failed
    public val isFailure: Boolean

    // true 如果 holder 表示 Closed(包括异常原因)
    public val isClosed: Boolean

    // true 如果操作成功(holder 不是 Failed)
    public val isSuccess: Boolean

    @Suppress("UNCHECKED_CAST")
    public fun getOrNull(): T? =
        if (isSuccess) holder as T else null

    public fun getOrThrow(): T {
        if (isSuccess) return holder as T
        // 如果是 Closed 并带有 cause,则抛出该异常
        if (holder is Closed && (holder as Closed).cause != null) {
            throw (holder as Closed).cause!!
        }
        error("Calling getOrThrow on a failed ChannelResult: $holder")
    }

    public fun exceptionOrNull(): Throwable? =
        (holder as? Closed)?.cause
}
  • getOrNull():
    返回成功的数据,失败或已关闭时返回 null
  • getOrThrow():
    成功时返回数据;如果失败且是带异常的关闭,抛出对应异常;否则抛出通用错误。
  • exceptionOrNull():
    如果因通道关闭且带有 cause,返回该异常;否则返回 null

3. receive() 还有一个特殊的版本,叫做 receiveCatching()

他也是挂起函数,它的返回值是 ChannelResult ,他在遇到异常的时候,不会抛出异常。而是和 tryReceive 一样,把异常包进ChannelResult中,返回给你自己处理,相当于是receivetryReceive的结合。

suspend fun <E> ReceiveChannel<E>.receiveCatching(): ChannelResult<E>
  • 特点

    • 是挂起函数,但不会抛异常。
    • 成功时返回包含元素的 ChannelResult.success(value)
    • 如果通道关闭,返回 ChannelResult.closed(cause? )
    • 如果中断或其他失败,也封装为 ChannelResult.failure(…),交给调用方处理。
  • 作用
    相当于将 receive() + tryReceive() 的行为合二为一,统一用 ChannelResult 来表示结果或异常。