11、Channel:Channel的概念与类型

协程之间的通信,说白了就是「数据怎么从一个协程传到另一个协程」。我刚开始接触 Kotlin 协程时,总觉得用 suspend 函数返回结果就够了。直到我在项目中遇到一个生产者-消费者场景——生产者不断产生数据,消费者需要按顺序处理,这时候 suspend 函数就有点力不从心了。

嗯,这时候 Channel 就派上用场了。

Channel 是什么?

Channel 是 Kotlin 协程提供的一种「管道」。你可以往里面塞数据,也可以从里面取数据。它有点像 Java 里的 BlockingQueue,但它是挂起的,不是阻塞的。

我个人的理解是:Channel 就是协程之间的「快递通道」。生产者往通道里放包裹,消费者从通道里取包裹。如果通道满了,生产者就得等一等;如果通道空了,消费者也得等一等。

核心概念: Channel 是一个热数据流(hot stream),不管有没有消费者,生产者都会生产数据。这一点和 Flow 不同,Flow 是冷的,没人收集就不生产。

Channel 的四种类型

Kotlin 提供了四种 Channel,区别在于缓冲区的大小和行为。我当年踩过一个坑——用错了类型导致内存暴涨,后来才搞明白它们的区别。

类型 缓冲区大小 行为特点 适用场景
Rendezvous 0 发送和接收必须同时发生,否则挂起 一对一同步通信
Buffered 指定大小 缓冲区未满时发送不挂起,满了则挂起 生产速度略快于消费速度
Conflated 1 只保留最新数据,旧数据被覆盖 只关心最新值,如 UI 状态
Unlimited 无限制 发送永不挂起,但可能 OOM 生产远快于消费,且数据量可控

Rendezvous Channel

这是默认类型,缓冲区大小为 0。发送方和接收方必须「碰头」才能完成数据传递。说白了,就是发送方调用 send() 时会挂起,直到有接收方调用 receive()。

val channel = Channel<Int>(Channel.RENDEZVOUS)

// 协程 A:发送
launch {
    println("准备发送 1")
    channel.send(1)
    println("发送完成")
}

// 协程 B:接收
launch {
    delay(1000)
    println("准备接收")
    val value = channel.receive()
    println("收到: $value")
}

运行结果会怎样?发送方会一直挂起,直到 1 秒后接收方开始接收。我在项目中用 Rendezvous 做过一个「握手协议」,确保两个协程严格同步执行。

Buffered Channel

这个最常用。你可以指定缓冲区大小,比如 10。发送方在缓冲区没满之前不会挂起,满了才挂起。

val channel = Channel<Int>(10) // 缓冲区大小为 10

launch {
    repeat(15) {
        channel.send(it)
        println("发送: $it")
    }
}

launch {
    repeat(15) {
        val value = channel.receive()
        println("接收: $value")
        delay(100)
    }
}

前 10 个数据会立即发送,第 11 个开始发送方会挂起,直到消费者取走数据腾出空间。我记得有一次做日志收集,生产速度远快于写入磁盘的速度,用 Buffered Channel 加一个合适的缓冲区就解决了。

我的建议: 缓冲区大小不要拍脑袋定。最好根据生产速度和消费速度的比值来估算。比如生产每秒 100 条,消费每秒 50 条,缓冲区至少得能容纳 1-2 秒的数据量。

Conflated Channel

这个类型很有意思。它只有一个缓冲区,而且只保留最新数据。如果发送方连续发了 3 条数据,消费者还没取走,那么前两条会被丢弃,只保留最后一条。

val channel = Channel<Int>(Channel.CONFLATED)

launch {
    repeat(5) {
        channel.send(it)
        println("发送: $it")
    }
}

launch {
    delay(500)
    val value = channel.receive()
    println("最终收到: $value") // 只会收到 4(最后一个)
}

我曾经在 UI 状态更新场景用过它。比如进度条更新,用户只关心最新的进度值,中间值丢了也无所谓。你想想看,如果进度从 10% 跳到 90%,中间那 20%、30% 其实没必要展示。

注意: Conflated Channel 会丢数据!如果你需要处理每一条数据,千万别用这个类型。我曾经有个同事用它做事件总线,结果丢了一堆点击事件,排查了半天。

Unlimited Channel

这个类型没有缓冲区上限。发送方永远不会挂起,但数据会堆积在内存里。说白了,就是生产者可以疯狂生产,消费者慢慢消费。

val channel = Channel<Int>(Channel.UNLIMITED)

launch {
    repeat(10000) {
        channel.send(it)
    }
}

launch {
    while (true) {
        val value = channel.receive()
        println("消费: $value")
        delay(10)
    }
}

这个类型要慎用。如果生产速度远大于消费速度,内存会一直增长,最终 OOM。我一般只在数据量可控、且生产速度偶尔爆发时用。比如用户快速点击按钮,我先把事件缓存起来,再慢慢处理。

Channel 的发送与接收

发送用 send(),接收用 receive()。这两个都是挂起函数,会挂起当前协程。

除了这两个,还有几个非挂起的变体:

  • trySend():尝试发送,不挂起。成功返回 true,失败返回 false。
  • tryReceive():尝试接收,不挂起。成功返回元素,失败返回 null。
  • receiveCatching():接收并捕获关闭状态,返回 ChannelResult。

我个人习惯在 UI 层用 trySend(),避免挂起导致界面卡顿。在后台处理层用 send(),确保数据一定被发送。

Channel 的关闭与迭代

Channel 用完后要关闭,否则消费者会一直挂起等待。关闭用 close() 方法。

val channel = Channel<Int>(10)

launch {
    for (i in 1..5) {
        channel.send(i)
    }
    channel.close() // 关闭通道
}

launch {
    // 用 for 循环迭代,通道关闭后自动退出
    for (value in channel) {
        println("收到: $value")
    }
    println("通道已关闭,迭代结束")
}

for 循环迭代 Channel 是最优雅的方式。它会一直接收数据,直到通道关闭。如果通道不关闭,for 循环会一直挂起等待。

避坑指南: 我曾经在项目中忘记关闭 Channel,导致消费者协程一直挂起,内存泄漏。后来我养成了一个习惯:谁创建 Channel,谁负责关闭。或者用 use 函数自动管理生命周期。

Channel 关闭后,再调用 send() 会抛出 ClosedSendChannelException。接收方可以通过 receiveCatching() 来优雅处理关闭状态。

val channel = Channel<Int>(10)
channel.close()

// 安全接收
val result = channel.receiveCatching()
if (result.isClosed) {
    println("通道已关闭,原因: ${result.closeCause}")
} else {
    println("收到: ${result.getOrNull()}")
}

知识体系总览

下面这张图总结了 Channel 的核心概念和类型选择逻辑:

Channel 知识体系 Channel 核心概念 Rendezvous 缓冲区=0,同步碰头 Buffered 指定缓冲区大小 Conflated 只保留最新数据 Unlimited 无上限,慎用 核心操作 send() / receive() 挂起函数 | trySend() / tryReceive() 非挂起 | receiveCatching() 安全接收 关闭与迭代 close() 关闭 | for (value in channel) 自动迭代 | receiveCatching() 处理关闭 谁创建谁关闭 | 用 for 循环迭代 | 避免 OOM

总结

Channel 是协程间通信的基础工具。四种类型各有用途:Rendezvous 适合同步场景,Buffered 最常用,Conflated 适合最新值场景,Unlimited 要慎用。发送接收用挂起函数,关闭用 close(),迭代用 for 循环。

我在项目中用得最多的是 Buffered 和 Conflated。前者做数据管道,后者做状态更新。记住一点:Channel 是热的,Flow 是冷的。选对工具,事半功倍。


公众号:蓝海资料掘金营,微信deep3321