协程间的通信管道 —— Kotlin Channel 详解

前言

我们先来介绍一下几个关键知识点:

  1. StateFlow:状态流,它是为 UI 层提供状态订阅的。

    每当状态的值发生变化,都会通知给订阅者。新订阅者总能收到最新的状态。

  2. SharedFlow :事件流,它是 StateFlow 的底层实现,提供事件订阅。

    默认情况下,订阅前发生的事件不会推送给新订阅者。

  3. Flow :冷数据流,它是SharedFlow 的底层,只有在被收集 (collect) 时才会执行。

  4. Channel :协程间协作工具,它是 Flow 的关键底层支撑,用于协程间传递数据。

    它的功能和 async 类似,只不过它是多条数据的 async。并且 async 是一次性的,而 Channel 能够多次发送数据。

我们从 Channel 开始。

Channel 是什么?

要理解它,我们先要来看看 asyncawait

我们可以使用 async 启动一个并发协程,并在其他协程中调用 await 获取到它的结果,它通过 API 的拆分,将异步协程的启动和结果的获取分开了。

kotlin 复制代码
fun main() = runBlocking<Unit> {
    val deferredFirst: Deferred<String> = async(Dispatchers.IO) {
        delay(5000) // 模拟耗时操作
        "Result-First"
    }

    val deferredSecond: Deferred<String> = async(Dispatchers.IO) {
        delay(3000) // 模拟耗时操作
        "Result-Second"
    }

    launch {
        // 模拟具有实际业务功能的挂起函数
        delay(2000)

        val result = deferredFirst.await() + "and" + deferredSecond.await()
        println("the result is $result")
    }
}

如果我们想要多次获取结果,需要一个持续的数据流,就不能使用 async 了,因为它只能返回一次,并且多次调用的结果都相同。

这时,可以使用 Channel,你可以把它看作是可以多次返回结果的 async

kotlin 复制代码
fun main() = runBlocking<Unit> {
    // produce 启动一个协程,并返回一个 ReceiveChannel
    val receiveChannel: ReceiveChannel<String> = produce(Dispatchers.Default) {
        while (isActive) {
            val currentTimeString =
                DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.US)
                    .format(
                        Date(System.currentTimeMillis())
                    )
            send(currentTimeString) // 使用 send 多次生产(发送)数据
            delay(1000)
        }
    }

    // 在另一个协程中消费数据
    launch {
        while (isActive) {
            // 获取新数据
            val time = receiveChannel.receive()
            println("Current Time is: $time")
            delay(1000)
        }
    }

    // 让程序运行 5 秒钟后停止
    delay(5000)
    coroutineContext.cancelChildren() // 取消 produce 和 launch
}

CoroutineScope.produce 也是协程构建器,它所启动的生产者协程,是用来给其他协程提供数据的。在不生产数据时,可当作普通的 launch 使用。

它内部提供了 ProducerScope,这个作用域同时实现了 CoroutineScopeSendChannel

kotlin 复制代码
public interface ProducerScope<in E> : CoroutineScope, SendChannel<E> {
    public val channel: SendChannel<E>
}

它会返回一个 ReceiveChannel(类似 async 的返回的 Deferred),可调用它的 receive() 挂起等待数据(类似 Deferred.await())。

运行结果:

less 复制代码
Current Time is: Oct 24, 2025, 1:49:58 PM
Current Time is: Oct 24, 2025, 1:49:59 PM
Current Time is: Oct 24, 2025, 1:50:00 PM
Current Time is: Oct 24, 2025, 1:50:01 PM
Current Time is: Oct 24, 2025, 1:50:02 PM

Channel 的本质:挂起式队列

ProducerScope 作用域对象和返回的 ReceiveChannel 对象其实是同一个对象。

进到 CoroutineScope.produce() 函数内部创建的 ProducerCoroutine 对象中,可以看到:这个对象继承了 ChannelCoroutine 类,ChannelCoroutine 又实现了 Channel 接口。

Channel 接口同时实现了 SendChannelReceiveChannel 接口:

kotlin 复制代码
public interface Channel<E> : SendChannel<E>, ReceiveChannel<E> {
    // ..
}

Channel 接口同时具备发送数据和接收数据的功能,这说明了在 produce() 函数发送数据和在其他协程中获取数据时,操作的是同一个对象。

produce 实际上是对 Channel 模型的一种封装,我们来看看 Channel 最纯粹、最核心的用法:

kotlin 复制代码
fun main() = runBlocking<Unit> {
    // 创建 Channel 对象
    val channel = Channel<String>()

    // 发送数据
    launch {
        delay(1000)
        channel.send("Hello, ")
        delay(3000)
        channel.send("Channel!")
    }

    // 接收数据
    launch {
        print(channel.receive())
        print(channel.receive())
    }

}

可以看出,Channel 本质上就是挂起队列,类似 Java 中的 BlockingQueue(阻塞队列)。

不过,当 Channel 队列满了之后,再往队列中添加元素(send())会挂起当前协程,而不是阻塞线程,直到队列有空位;当队列为空时,尝试从队列中获取元素(receive())会挂起当前协程,而不是阻塞线程,直到队列有元素插入。

知道了它的队列本质,我们也就知道了 Channel 并不适合做可订阅的事件流。它的一个元素最多只能被一个消费者接收,当订阅者超过一个时,会导致每个订阅者都无法接收到完整的事件序列:

kotlin 复制代码
fun main() = runBlocking<Unit> {
    // 创建 Channel 对象
    val channel = Channel<String>()

    launch(Dispatchers.IO) {
        channel.send("event-1")
        channel.send("event-2")
        channel.send("event-3")
    }

    launch {
        for (data in channel) { // receive() 的简便写法,遍历时会将当前协程挂起
            println("A received: $data")
            delay(100)
        }
    }

    launch {
        for (data in channel) { // 同样是挂起式的遍历
            println("B received: $data")
            delay(100)
        }
    }

}

运行结果:

less 复制代码
A received: event-1
B received: event-2
A received: event-3

在开发中,我们会使用 SharedFlow 来实现事件流的订阅。

那么,Channel 有什么用?

首先它作为 Flow 的底层支持,很多 Flow 的操作符就是使用的 Channel。其次,在模块内部,能确保只有一个订阅者的时候,我们就可以使用它,因为它简单高效。

容量、策略与生命周期

容量 (Capacity)

默认情况下,Channel 队列的长度是 0。创建 Channel 使用的容量值为 RENDEZVOUS= 0),它的意思是"会合",也就是 send()receive() 会合时,才会交接数据,完全没有缓冲。

这意味着发送数据时会一直挂起,直到其他协程接收数据;其他协程接收数据时也会一直挂起,直到当前协程发送数据。

我们可以给 Channel 指定容量:

kotlin 复制代码
// 缓冲区容量为 10
val channel = Channel<Int>(capacity = 10)

此时,调用 send() 会立即返回(将数据放入缓冲区)。直到缓冲区满了后,之后调用的 send() 才会挂起。

我们也可以使用 Channel 提供的容量值,UNLIMITEDBUFFERED

UNLIMITED 的值为 Int.MAX_VALUE,这个值应该慎重选择;而 BUFFERED 的容量默认是 64。

kotlin 复制代码
// Channel.kt
internal val CHANNEL_DEFAULT_CAPACITY = systemProp(DEFAULT_BUFFER_PROPERTY_NAME,
    64, 1, UNLIMITED - 1
)

缓冲溢出策略

当缓冲区满了之后,再次发送数据默认会挂起 ,这就是缓冲溢出。当然这也是背压的体现:下游处理速度跟不上上游发送速度。

我们可以改变这个策略:

kotlin 复制代码
// 默认挂起当前协程
val channel1 = Channel<String>(capacity = 10, onBufferOverflow = BufferOverflow.SUSPEND)

// 丢弃最旧的数据,将新数据放进队尾
val channel2 = Channel<String>(capacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST) 

// 丢弃当前要发送的新数据
val channel3 = Channel<String>(capacity = 10, BufferOverflow.DROP_LATEST)

CONFLATED

其实,Channel 还提供了另一种预设的容量值:CONFLATED

它是融合、合并的意思,它的容量只有 1,同时缓冲溢出的策略是 DROP_OLDEST

这意味着它只会关心最新的数据,生产者会不断使用新数据替换缓冲区未被消费的旧数据,消费者永远会拿到最新数据。

这正是 Flow 中的 conflate() 操作符的实现原理。

注意:当使用 CONFLATED 时,缓冲策略必须不填,或者为默认值 SUSPEND

Channel 的关闭

Channel 的关闭有两个函数,分别是 SendChannel.close()ReceiveChannel.cancel()。前者由生产者调用,表示没有更多数据需要发送了;后者由消费者调用,表示不再需要数据了。

close()

SendChannel 接口的内部有一个 isClosedForSend 属性,表示是否已经关闭了发送功能。这个属性的默认值为 false,当我们调用了 close() 函数后,它会变为 true

当这个标志为 true 时,禁止再次发送数据(调用 send() 会抛出 ClosedSendChannelException),但我们还是可以调用 receive() 来接收数据,直到缓冲区为空。

当缓冲区为空后,ReceiveChannel.isClosedForReceive 会变为 true,此时,再调用 receive() 来接收数据会抛出 ClosedReceiveChannelException。最后,for 循环会自动正常结束。

kotlin 复制代码
fun main() = runBlocking<Unit> {
    val channel = Channel<String>(capacity = 2)

    // 生产者
    launch(Dispatchers.Default) {
        channel.send("event-1")
        channel.send("event-2")
        channel.send("event-3")
        channel.close()
    }

    // 消费者
    launch(Dispatchers.Default) {
        delay(1000)
        try {
            for (data in channel) {
                println("A received: $data")
                delay(1000)
            }
        } catch (e: Exception) {
            // 不应该捕获到异常
            println("A finished with exception: $e")
        }

        println("A: isClosedForReceive = ${channel.isClosedForReceive}")

        // 尝试再次接收
        try {
            val data = channel.receive()
            println("A received after close: $data")
        } catch (e: Exception) {
            println("A after close with exception: $e")
        }
        println("A finished")
    }
}

运行结果:

less 复制代码
A received: event-1
A received: event-2
A received: event-3
A: isClosedForReceive = true
A after close with exception: kotlinx.coroutines.channels.ClosedReceiveChannelException: Channel was closed
A finished

cancel()

当我们调用 ReceiveChannel.cancel() 时,它会关闭双端,也就是将 isClosedForReceiveisClosedForSend 标志都设为 true

当发送或接收数据时,会抛出 CancellationException 异常,并且缓冲区那些还未被消费的数据会被丢弃。

kotlin 复制代码
fun main() = runBlocking<Unit> {
    val channel = Channel<String>(capacity = 10)

    // 生产者
    launch(Dispatchers.Default) {
        try {
            channel.send("event-1")
            channel.send("event-2")
            delay(2000)
            channel.send("event-3") // 永远不会被发送
        } catch (e: Exception) {
            // 这里会捕捉到 CancellationException
            println("Producer caught exception: $e")
        }
    }

    // 消费者
    launch(Dispatchers.Default) {
        try {
            println("A received: ${channel.receive()}")
            println("A received: ${channel.receive()}")

            channel.cancel()

            for (data in channel) {
                println("A received in loop: $data")
            }
        } catch (e: Exception) {
            // 这里会捕捉到 CancellationException
            println("A finished with exception: $e")
        }
        println("A finished")
    }
}

运行结果:

less 复制代码
A received: event-1
A received: event-2
A finished with exception: java.util.concurrent.CancellationException: Channel was cancelled
A finished
Producer caught exception: java.util.concurrent.CancellationException: Channel was cancelled

资源清理

如果在 Channel 中传递的数据是文件句柄或是数据库连接等需要关闭的资源,直接丢弃(包括因缓冲策略被丢弃的数据)可能会导致资源泄露。

这时,我们可以在创建 Channel 传入 onUndeliveredElement 回调,从而安全地关闭资源:

kotlin 复制代码
val channel = Channel<FileWriter>(
    capacity = 3,
    onBufferOverflow = BufferOverflow.DROP_OLDEST,
    onUndeliveredElement = { fileWriter ->
        fileWriter.close()
    })

actor 模式

produce 封装了 Channel 的创建和发送,而 actor 函数则是封装了创建和接收的逻辑。

它内部提供的是 ActorScope 作用域,返回的对象类型是 SendChannel

kotlin 复制代码
fun main() = runBlocking<Unit> {
    // 启动协程,返回 SendChannel
    val actorChannel: SendChannel<String> = actor(Dispatchers.Default) {
        // 协程内部是接收端
        for (data in channel) {
            println("Received Message is: $data")
        }
        println("Actor is done.")
    }

    // 发送消息
    launch(Dispatchers.IO) {
        actorChannel.send("Message 1")
        delay(100)
        actorChannel.send("Message 2")

        actorChannel.close()
    }
}

运行结果:

less 复制代码
Received Message is: Message 1
Received Message is: Message 2
Actor is done.
相关推荐
菠菠萝宝18 小时前
【AI应用探索】-10- Cursor实战:小程序&APP - 下
人工智能·小程序·kotlin·notepad++·ai编程·cursor
RainbowC019 小时前
从Dalvik字节码角度优化安卓编码
android·java/jvm
河铃旅鹿19 小时前
Android开发-java版:布局
android·笔记·学习
Meteors.20 小时前
安卓进阶——RxJava
android·rxjava
drsonxu1 天前
Android开发自学笔记 --- 构建简单的UI视图
android·compose
onthewaying1 天前
在Android平台上使用Three.js优雅的加载3D模型
android·前端·three.js
带电的小王1 天前
Android设备:无busybox工具解决
android·busybox
默契之行1 天前
为什么要使用 .asStateFlow() 而不是直接赋值?
kotlin
一 乐1 天前
个人健康系统|健康管理|基于java+Android+微信小程序的个人健康系统设计与实现(源码+数据库+文档)
android·java·数据库·vue.js·spring boot·生活
百锦再1 天前
第14章 智能指针
android·java·开发语言·git·rust·go·错误