Kotlin管道Channel构造函数参数capacity值RENDEZVOUS与UNLIMITED

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)

更安全。

因为它天然有背压。

适合:

  1. 生产者不应该无限快于消费者;

  2. 任务必须被真正接收后才算发送成功;

  3. 不希望内存中堆积大量事件;

  4. 希望消费端处理能力能反向约束生产端;

  5. 任务处理、流水线、协程间同步。

谨慎使用:Channel.UNLIMITED

UNLIMITED 适合非常少数场景,比如:

  1. 事件数量有严格上限;

  2. 生产速度可控;

  3. 生命周期很短;

  4. 内存风险可接受;

  5. 绝不能阻塞发送方;

  6. 有额外的限流、降级或监控。

例如某些 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. RENDEZVOUSsend 挂起是不是坏事?

不是。

很多时候这是好事。

因为它表示:

复制代码
数据没有被无限堆积,发送方会等待消费者准备好。

例如任务处理:

复制代码
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,一定要问自己几个问题

  1. 生产速度是否可能超过消费速度?

  2. 最坏情况下会积压多少元素?

  3. 每个元素占多少内存?

  4. 页面或作用域取消后,生产者是否还会继续发送?

  5. 是否有监控 Channel 积压?

  6. 是否允许丢弃旧数据?

  7. 是否应该用 SharedFlowStateFlow 或有限容量 Channel 替代?

如果这些问题答不清楚,不建议用 UNLIMITED

19.3 不要用 UNLIMITED 掩盖挂起问题

有些时候开发者发现:

复制代码
channel.send(event)

挂起了,于是改成:

复制代码
Channel.UNLIMITED

这样表面上问题消失了。

但实际上可能只是把问题从:

复制代码
发送方挂起

变成了:

复制代码
内存中积压

更好的做法是先分析:

  1. 为什么没有消费者?

  2. 消费者生命周期是否正确?

  3. 是否应该用 trySend

  4. 是否应该允许丢弃?

  5. 是否应该改用 SharedFlow

  6. 是否应该设置有限 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 个额外缓冲,整体仍然有背压。

如果 mChannelChannel.UNLIMITED

复制代码
Channel 自身可能无限缓存,Flow 的 buffer(10) 不能限制整体积压。

所以实际开发中,一句话建议是:

不确定时优先使用默认 Channel() 或有限容量 Channel;只有在明确知道数据量可控、发送方绝不能挂起,并且能接受内存积压风险时,才考虑 Channel.UNLIMITED

一个朋友创建的AI大模型知识站点,内容优质很不错。

相关推荐
plainGeekDev1 小时前
Timer → Coroutines
android·java·kotlin
Coffeeee1 小时前
Android17应用内存限制--App:我人不舒服,系统:那你走吧
android·google·kotlin
问心无愧05132 小时前
ctf show web入门101
android·前端·笔记
一池秋_2 小时前
chroot-debian一键部署
android·容器·debian
超梦dasgg2 小时前
APP 壳、加固、脱壳 完整通俗讲解(安卓为主,兼顾 iOS)
android·ios
猪脚饭还是好吃的2 小时前
【分享】C4droid 安卓C++编译器 手机编程超便捷
android·c++·智能手机
AI浩2 小时前
【数据处理】基于 SAM3 的 LabelMe 标注统一校正方法
android·开发语言·kotlin
恋猫de小郭2 小时前
真正的跨平台 AI 自动化框架,甚至还支持鸿蒙
android·前端·flutter
私人珍藏库2 小时前
【Android】 VidFetch一键下载各大平台视-内置播放器
android·app·工具·软件·多功能