协程间的通信管道 —— 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.
相关推荐
阿巴斯甜14 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker14 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952715 小时前
Andorid Google 登录接入文档
android
黄林晴17 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android