Channel 和 Flow 简介与对比
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. 几个需要注意的地方
-
StateFlow是继承自SharedFlow的一个子接口,专门用来做“状态持有者”。 -
SharedFlow继承 自Flow,并在其基础上增加了发射(emit)和重放(replay)等能力。 -
Channel 不仅仅是多路版的 async
- Channel 更底层,提供了多种 buffer 策略,可直接进行 send/receive;而 async 只是开启一个协程并返回 Deferred。
-
性能与场景
- 若只做“跨线程”一次性数据传递,可直接用
Flow(flowOn切换调度);若需要细粒度的 back-pressure 或挂起特性,可考虑 Channel。
- 若只做“跨线程”一次性数据传递,可直接用
SendChannel 和 ReceiveChannel 的定义、核心 API,以及二者的对比
Channel的父类包含SendChannel 和 ReceiveChannel, 拆分成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?):取消接收端(并关闭底层通道)。
三、对比与职责分离
-
职责分离
- 将一个完整的
Channel<E>拆分成“能发”和“能收”两部分,分别由生产者和消费者持有,利于职责清晰、类型安全。
- 将一个完整的
-
阻塞与否
send/receive会挂起;trySend/tryReceive不挂起,立即返回结果。
-
异常处理
- 挂起版在失败时直接抛异常;尝试版则用
ChannelResult把状态与异常封装起来,由调用方自行检查。
- 挂起版在失败时直接抛异常;尝试版则用
-
关闭行为
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()只是“暂停”当前协程直到结果就绪,不会阻塞底层线程。- 结果产生后,再次调用同一个
Deferred的await(),返回值是相同的——它只执行了一次。
在需要持续轮询服务器以保持数据更新的场景下,虽然常见的方案有 HTTP 轮询、WebSocket 或 SSE(Server-Sent Events),但 async 由于只会产生一次性结果,并不适合长期运行。更好的做法是:
- 使用 Flow 或 SharedFlow,它们可以在后台不断发射新数据;
- 或者用 Channel 的
produce { … }+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 / Deferred | produce / ReceiveChannel |
|---|---|---|
| 执行次数 | 代码块只执行一次 | 可以在循环中多次执行(send 多次) |
| 返回类型 | Deferred<T>,一次性拿到结果 | ReceiveChannel<T>,多次 receive |
| 获取数据 | await() 多次调用结果相同 | receive() 每次调用拿到下一个数据 |
| 典型用途 | 并发请求一次性结果 | 实时或周期性数据推送 |
总结:
- 把
async看作“单次生产”的异步任务,它返回一个只能完成一次计算的Deferred; - 把
produce(底层基于 Channel)看作“多次生产”的异步数据流,它通过send/receive构建了一个可反复获取新数据的事件流模型。
这样,使用 produce + receive 就能在一个协程内部持续生产数据,其他协程通过 ReceiveChannel 不断取用,非常适合轮询或实时更新的场景。
async 和 produce 的本质区别在于:
- 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 函数里边的 CoroutineScope 的 job 对象与 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(),数据从通道取出。
当在不同协程里分别进行 send 和 receive 调用时,数据就能从一个协程发送到另一个协程。
而 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,不同点在于:
BlockingQueue在满或空时会阻塞线程;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
send和receive都是 挂起函数,只有协程被挂起,不会阻塞底层线程。- 因此,使用
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 支持三种溢出处理策略:
-
SUSPEND(默认)
- 挂起发送方直到有缓冲空间。
-
DROP_OLDEST
- 丢弃最旧的一条消息,再把新消息放入队列。
-
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关闭; - 丢弃缓冲区内尚未消费的消息;
- 所有挂起的
send或receive调用都会被恢复并抛出CancellationException; SendChannel.isClosedForSend和ReceiveChannel.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. 异常类型
| 操作 | 异常类型 | 什么时候抛 |
|---|---|---|
在已关闭通道上 send | ClosedSendChannelException | close() 之后调用 |
在已关闭且空通道上 receive | ClosedReceiveChannelException | 缓冲消耗完毕后再调用 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()都会抛出一个包装了你指定cause的ClosedReceiveChannelException,便于在上层捕获专属的错误语义。
2. cancel(cause: CancellationException):接收端发起的“彻底取消”
-
当消费者不再需要后续数据时,调用
cancel():receiveChannel.cancel(CancellationException("No longer needed")) -
效果:
isClosedForSend和isClosedForReceive同时置为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都会被触发:cancel()立即丢弃所有挂起和缓冲中的元素;- 缓冲区溢出且策略为
DROP_OLDEST或DROP_LATEST; - 通道执行
close(cause)后,还有尚未receive的元素。
这样,
- 用
close(cause)定制流结束的异常语义; - 用
cancel()实现“彻底中断并清理”; - 用
onUndeliveredElement回调,确保所有资源都能在元素无法正常到达消费者时得到释放。
send / receive还有一对兄弟函数:trySend / tryReceive
1. send / receive vs. trySend / tryReceive
-
send和receive-
都是 挂起函数(suspending),会在以下情况下挂起协程:
send:当通道缓冲区已满时,挂起直到有空位或通道关闭。receive:当通道为空时,挂起直到有元素或通道关闭。
-
失败时会 抛出异常(例如
ClosedSendChannelException、ClosedReceiveChannelException)。
-
-
trySend和tryReceive-
不是挂起函数,不会阻塞线程。
-
调用时会 立即返回一个
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中,返回给你自己处理,相当于是receive和tryReceive的结合。
suspend fun <E> ReceiveChannel<E>.receiveCatching(): ChannelResult<E>
-
特点:
- 是挂起函数,但不会抛异常。
- 成功时返回包含元素的
ChannelResult.success(value); - 如果通道关闭,返回
ChannelResult.closed(cause? ); - 如果中断或其他失败,也封装为
ChannelResult.failure(…),交给调用方处理。
-
作用:
相当于将receive()+tryReceive()的行为合二为一,统一用ChannelResult来表示结果或异常。