Kotlin管道Channel构造函数参数capacity值RENDEZVOUS与UNLIMITED
摘要:Kotlin协程中的Channel构造函数参数capacity默认为RENDEZVOUS(0),而非UNLIMITED(Int.MAX_VALUE),这是重要的设计差异。RENDEZVOUS表示无缓冲通道,发送和接收必须同步完成,否则会挂起,形成天然背压机制;而UNLIMITED允许无限缓存数据,可能导致内存问题。开发者通常应优先使用默认RENDEZVOUS或有限容量通道,仅在明确数据量可控且需要避免发送方挂起时才考虑UNLIMITED。本文详细比较了两者在send/receive行为、背压机制、内存风险等方面的差异。
public fun <E> Channel(
capacity: Int = RENDEZVOUS,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E>
其中:
Channel.RENDEZVOUS = 0
Channel.UNLIMITED = Int.MAX_VALUE
也就是说:
val channel = Channel<Int>()
默认等价于:
val channel = Channel<Int>(capacity = Channel.RENDEZVOUS)
而不是:
Channel.UNLIMITED
这是非常重要的设计差异。
1. 一句话区别
Channel.RENDEZVOUS
val channel = Channel<Int>()
// 等价于
val channel = Channel<Int>(Channel.RENDEZVOUS)
它是一个无缓冲 Channel。
发送方和接收方必须"碰头"才能完成数据传递。
可以理解为:
没有人接收时,send 会挂起;没有人发送时,receive 会挂起。
Channel.UNLIMITED
val channel = Channel<Int>(Channel.UNLIMITED)
它是一个无限缓冲 Channel。
发送方基本不会因为缓冲区满而挂起。
可以理解为:
只要内存还够,send 可以一直往里面塞数据。
2. RENDEZVOUS = 0 到底是什么意思?
RENDEZVOUS 这个词本身有"会合、碰头"的意思。
Channel.RENDEZVOUS 的核心语义是:
发送方 send 和接收方 receive 必须配对完成。
它没有内部缓冲区。
示例:
val channel = Channel<Int>() // RENDEZVOUS
launch {
println("before send")
channel.send(1)
println("after send")
}
delay(3000)
launch {
println("before receive")
val value = channel.receive()
println("receive: $value")
}
输出大致是:
before send
// 等待 3 秒
before receive
receive: 1
after send
注意:
channel.send(1)
在没有接收者之前会挂起。
3. UNLIMITED 是什么行为?
val channel = Channel<Int>(Channel.UNLIMITED)
launch {
repeat(1_000_000) {
channel.send(it)
println("sent $it")
}
}
在 UNLIMITED 下,send 基本不会因为容量问题挂起。
即使暂时没有消费者,也可以继续发送。
它内部会不断缓存元素。
所以:
生产者可以一直生产
消费者可以之后慢慢消费
这听起来很方便,但风险也非常明显:
如果生产速度长期大于消费速度,内存会持续上涨,最终可能 OOM。
4. 两者最重要的差异:是否有背压
RENDEZVOUS:天然背压
val channel = Channel<Int>()
如果消费者慢,生产者就会被迫慢下来。
例如:
val channel = Channel<Int>()
launch {
repeat(100) {
println("send start: $it")
channel.send(it)
println("send done: $it")
}
}
launch {
for (item in channel) {
delay(1000)
println("receive: $item")
}
}
每次 send 都要等接收方接住。
所以生产者不会无限堆积数据。
这是背压:
消费者慢 -> 发送方挂起 -> 生产速度被限制
UNLIMITED:几乎没有背压
val channel = Channel<Int>(Channel.UNLIMITED)
如果消费者慢,生产者不会被消费者拖慢,而是继续把数据放入 Channel。
val channel = Channel<Int>(Channel.UNLIMITED)
launch {
repeat(100_000) {
channel.send(it)
println("send done: $it")
}
}
launch {
for (item in channel) {
delay(1000)
println("receive: $item")
}
}
生产者可能很快发送完 100000 条。
消费者则慢慢处理。
结果就是:
大量数据堆积在 Channel 内部
5. 对 send() 的影响
RENDEZVOUS
channel.send(value)
可能挂起,直到有接收者。
行为:
没有 receiver -> send 挂起
有 receiver -> send 立即完成
UNLIMITED
channel.send(value)
通常不会因为容量问题挂起。
行为:
没有 receiver -> 存入内部队列
有 receiver -> 直接交给 receiver 或入队
但注意,不是绝对永远不会挂起。
如果 Channel 已关闭,发送会失败或抛异常。
如果内存不足,也可能导致严重问题。
6. 对 trySend() 的影响
trySend 是非挂起发送。
RENDEZVOUS
val result = channel.trySend(value)
因为没有缓冲区,所以只有在刚好有接收者等待时,才可能成功。
否则大概率失败:
val channel = Channel<Int>()
val result = channel.trySend(1)
println(result.isSuccess) // 通常是 false
因为没有人正在 receive(),也没有地方缓存。
UNLIMITED
val channel = Channel<Int>(Channel.UNLIMITED)
val result = channel.trySend(1)
println(result.isSuccess) // 通常是 true
因为可以直接放入内部队列。
只要 Channel 没关闭,通常都会成功。
7. 对 receive() 的影响
这点两者相似。
如果没有数据:
channel.receive()
都会挂起。
区别在于数据来源不同。
RENDEZVOUS
接收方等待发送方:
receive 挂起,直到某个 send 出现
UNLIMITED
接收方优先从内部缓冲队列取:
如果队列有数据 -> 立即取
如果队列没数据 -> 挂起等待 send
8. 和 receiveAsFlow().buffer(10) 的关系
mChannel.receiveAsFlow()
.onEach {
println("onEach: $it")
}
.buffer(10)
.collect {
delay(1000)
println("collect: $it")
}
现在假设 mChannel 是默认构造:
val mChannel = Channel<Int>()
也就是:
val mChannel = Channel<Int>(Channel.RENDEZVOUS)
这个时候情况很有意思。
虽然原始 Channel 没有缓冲区,但是 Flow 的:
.buffer(10)
会引入一个 Flow 内部缓冲区。
大致结构变成:
producer
|
| send
v
Channel.RENDEZVOUS,无缓冲
|
| receiveAsFlow 接收
v
onEach
|
v
Flow buffer,容量 10
|
v
collect,下游慢慢消费
所以,即使 mChannel 是无缓冲的,receiveAsFlow().buffer(10) 仍然会让 Flow 上游提前从 Channel 接收数据,并放入 Flow 的 buffer。
换句话说:
Channel 本身没缓冲
但 Flow 管道额外有 10 个缓冲位
因此生产者可能观察到:
我好像可以连续 send 多个元素
原因不是 Channel 默认有容量,而是 Flow 的 buffer(10) 在下游之前接住了这些元素。
9. 默认 Channel() + buffer(10) 的实际容量感知
例如:
val mChannel = Channel<Int>() // RENDEZVOUS
launch {
repeat(100) {
println("send start: $it")
mChannel.send(it)
println("send done: $it")
}
}
mChannel.receiveAsFlow()
.onEach {
println("onEach: $it")
}
.buffer(10)
.collect {
delay(1000)
println("collect: $it")
}
这里不是说总容量只有 0。
也不是说 buffer(10) 改变了 mChannel 的容量。
更准确地说:
mChannel 本身:0 缓冲
Flow buffer:10 缓冲
collect 正在处理:约 1 个
所以整个链路上可能有:
约 10 个在 Flow buffer 中
约 1 个正在 collect 中处理
上游 send 会在这些缓冲空间被消耗完之后挂起。
10. 如果换成 Channel.UNLIMITED 会发生什么?
val mChannel = Channel<Int>(Channel.UNLIMITED)
mChannel.receiveAsFlow()
.onEach {
println("onEach: $it")
}
.buffer(10)
.collect {
delay(1000)
println("collect: $it")
}
这个时候整体结构是:
producer
|
v
Channel.UNLIMITED,无限缓冲
|
v
receiveAsFlow
|
v
onEach
|
v
Flow buffer,容量 10
|
v
collect
此时即使 Flow buffer 只有 10,也挡不住生产者不断往 mChannel 里面塞数据。
因为:
Channel.UNLIMITED 自己就可以无限缓存
所以整体积压可能是:
Channel 内部无限积压
+
Flow buffer 10 个
+
collect 正在处理的元素
这就是最大的实际差异。
11. 两者在实际开发中的典型选择
推荐默认:Channel.RENDEZVOUS
如果不确定该用什么,默认的:
val channel = Channel<Event>()
通常比:
val channel = Channel<Event>(Channel.UNLIMITED)
更安全。
因为它天然有背压。
适合:
-
生产者不应该无限快于消费者;
-
任务必须被真正接收后才算发送成功;
-
不希望内存中堆积大量事件;
-
希望消费端处理能力能反向约束生产端;
-
任务处理、流水线、协程间同步。
谨慎使用:Channel.UNLIMITED
UNLIMITED 适合非常少数场景,比如:
-
事件数量有严格上限;
-
生产速度可控;
-
生命周期很短;
-
内存风险可接受;
-
绝不能阻塞发送方;
-
有额外的限流、降级或监控。
例如某些 UI 层轻量事件,短时间内不想阻塞发送方,会考虑。
但在服务端、任务队列、消息处理、数据同步中,UNLIMITED 要非常谨慎。
12. 一个非常重要的误区
很多人会觉得:
Channel()
是一个普通队列。
但实际上不是。
默认的:
Channel()
是:
Channel(Channel.RENDEZVOUS)
它没有缓冲区。
它更像是:
一个挂起式交接点
而不是:
一个可以堆积数据的队列
如果没有接收者,发送者就会挂起。
13. RENDEZVOUS 和阻塞队列的类比
如果类比 Java 并发工具:
Channel.RENDEZVOUS
有点像:
SynchronousQueue
即不存储元素,发送和接收要直接交接。
Channel.UNLIMITED
有点像:
LinkedBlockingQueue 没有限制容量的用法
或者说逻辑上的无界队列。
当然 Kotlin Channel 是挂起式的,不是线程阻塞式的。
14. 对协程调度和资源占用的影响
RENDEZVOUS
当没有接收方时:
channel.send(value)
会挂起当前协程。
注意,这是协程挂起,不是阻塞线程。
所以不会一直占用线程。
但是业务流程会停在那里,直到有人接收。
优点:
内存安全,天然限速
缺点:
如果没有消费者,生产协程会一直挂起
UNLIMITED
发送方不会因为消费者慢而挂起。
优点:
生产方很顺畅,不容易被消费端拖慢
缺点:
消费者慢时,数据持续积压,内存风险大
15. Android 开发中特别注意
如果在 Android 里写:
private val channel = Channel<UiEvent>()
然后在 ViewModel 中:
viewModelScope.launch {
channel.send(UiEvent.ShowToast("hello"))
}
如果此时页面还没有开始 collect,或者 collect 已经取消了,那么 send 可能会挂起。
比如:
val flow = channel.receiveAsFlow()
页面侧:
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.flow.collect {
handleEvent(it)
}
}
}
当页面不在 STARTED 状态时,collector 会取消。
这时如果 ViewModel 继续:
channel.send(event)
默认 RENDEZVOUS 可能会挂起,直到下一次有 collector。
这在某些场景是想要的,也可能不是想要的。
如果不希望挂起,可以考虑:
channel.trySend(event)
或者使用:
Channel.BUFFERED
或者更适合 UI 状态的:
SharedFlow
StateFlow
具体取决于事件语义。
16. RENDEZVOUS 下 send 挂起是不是坏事?
不是。
很多时候这是好事。
因为它表示:
数据没有被无限堆积,发送方会等待消费者准备好。
例如任务处理:
val channel = Channel<Task>()
launch {
for (task in tasks) {
channel.send(task)
}
}
launch {
for (task in channel) {
process(task)
}
}
如果 process(task) 很慢,生产任务的一方会自动慢下来。
这能避免内存中堆积大量任务。
17. UNLIMITED 最大的问题:隐藏消费瓶颈
使用:
Channel.UNLIMITED
的时候,生产者很快,消费者很慢,短时间内程序似乎没问题。
因为发送都成功了。
但是问题被隐藏到了内存里:
Channel 内部队列越来越长
处理延迟越来越高
内存越来越大
最终可能 OOM
所以 UNLIMITED 的危险在于:
它不会及时暴露消费者跟不上生产者的问题。
而 RENDEZVOUS 会很快暴露:
send 挂起了,说明消费者跟不上。
18. 和 buffer(10) 搭配时的关键结论
情况一:默认 Channel
val mChannel = Channel<Int>() // RENDEZVOUS
mChannel.receiveAsFlow()
.onEach { }
.buffer(10)
.collect { }
整体效果:
Channel 自身不缓存
Flow buffer 缓存 10 个
生产者最终会被 buffer 和消费者速度反压
这是相对安全的。
情况二:无限 Channel
val mChannel = Channel<Int>(Channel.UNLIMITED)
mChannel.receiveAsFlow()
.onEach { }
.buffer(10)
.collect { }
整体效果:
Channel 自身可以无限缓存
Flow buffer 只是额外的 10 个
生产者基本不会被消费者速度反压
这是有内存风险的。
19. 实际开发建议
19.1 默认优先考虑 RENDEZVOUS 或有限容量
如果没有非常明确的原因,不建议一上来就用:
Channel.UNLIMITED
更建议:
Channel()
或者:
Channel(capacity = 64)
Channel(capacity = 100)
Channel(capacity = Channel.BUFFERED)
这样至少有背压或者有限缓冲。
19.2 如果使用 UNLIMITED,一定要问自己几个问题
-
生产速度是否可能超过消费速度?
-
最坏情况下会积压多少元素?
-
每个元素占多少内存?
-
页面或作用域取消后,生产者是否还会继续发送?
-
是否有监控 Channel 积压?
-
是否允许丢弃旧数据?
-
是否应该用
SharedFlow、StateFlow或有限容量 Channel 替代?
如果这些问题答不清楚,不建议用 UNLIMITED。
19.3 不要用 UNLIMITED 掩盖挂起问题
有些时候开发者发现:
channel.send(event)
挂起了,于是改成:
Channel.UNLIMITED
这样表面上问题消失了。
但实际上可能只是把问题从:
发送方挂起
变成了:
内存中积压
更好的做法是先分析:
-
为什么没有消费者?
-
消费者生命周期是否正确?
-
是否应该用
trySend? -
是否应该允许丢弃?
-
是否应该改用
SharedFlow? -
是否应该设置有限 buffer?
20. 简单对比表
| 特性 | Channel.RENDEZVOUS / Channel() |
Channel.UNLIMITED |
|---|---|---|
| 容量 | 0 | 理论无限 |
| 是否缓存元素 | 不缓存 | 缓存 |
send 是否可能挂起 |
很容易挂起 | 通常不因容量挂起 |
| 是否有背压 | 有,天然背压 | 基本没有 |
| 内存风险 | 低 | 高 |
| 消费者慢时 | 生产者变慢 | 数据堆积 |
trySend 成功率 |
没有接收者时通常失败 | 未关闭时通常成功 |
| 适合场景 | 同步交接、任务流水线、需要背压 | 生产不可阻塞且数据量可控的场景 |
| 默认值 | 是 | 否 |
21. 总结
Channel() 默认是:
Channel(Channel.RENDEZVOUS)
它的 capacity = 0,表示无缓冲 Channel。
它和 Channel.UNLIMITED 的关键差异是:
RENDEZVOUS:发送和接收必须会合,消费者慢会反压生产者。
UNLIMITED:发送方可以持续发送,消费者慢会导致数据在内存中积压。
代码:
mChannel.receiveAsFlow()
.onEach { }
.buffer(10)
如果 mChannel 是默认的 Channel():
Channel 自身不缓存,Flow 的 buffer(10) 提供 10 个额外缓冲,整体仍然有背压。
如果 mChannel 是 Channel.UNLIMITED:
Channel 自身可能无限缓存,Flow 的 buffer(10) 不能限制整体积压。
所以实际开发中,一句话建议是:
不确定时优先使用默认
Channel()或有限容量 Channel;只有在明确知道数据量可控、发送方绝不能挂起,并且能接受内存积压风险时,才考虑Channel.UNLIMITED。