【Kotlin 协程修仙录 · 炼虚境 · 初阶】 | 虚空造物:Channel 基础与协程间通信的管道艺术

前言

化神大圆满,你已驾驭 Flow 的异常天劫。冷流热流在你手中变幻自如,数据如长江大河,奔流不息。然而,你是否遇到过这样的场景?

你需要在两个协程之间传递数据 ------比如一个协程负责下载文件,另一个协程负责处理下载进度并更新 UI。你用 SharedFlow 实现了,但总觉得哪里不对劲:SharedFlow 是为了多播事件 设计的,你却在用它做一对一的任务传递 。更糟的是,你发现无法暂停上游生产------当 UI 来不及处理时,进度事件要么堆积,要么丢弃。

你需要的是一种更原始的、双向控制的 协程间通信机制------它允许一个协程发送 数据,另一个协程接收 数据;当接收方来不及处理时,发送方可以挂起等待 ;当没有数据时,接收方也可以挂起等待 。这正是 Kotlin 协程为你准备的管道 ------Channel

Channel 是一个挂起式的、可缓冲的、协程间的通信原语 。它允许一个协程通过 send 发送数据,另一个协程通过 receive 接收数据。Channel 是协程世界中的 BlockingQueue,但它是非阻塞的------发送和接收都是挂起函数。

本讲是炼虚境的初阶修炼。你将:

  • 彻底搞懂 Channel 的概念,以及它与 Flow/SharedFlow 的本质区别。
  • 掌握 Channel 的四种容量策略:RENDEZVOUSBUFFEREDCONFLATEDUNLIMITED
  • 学会用 produce 构建器简化生产者-消费者模式。
  • 理解 Channel 的关闭与迭代。
  • 在 Android 中用 Channel 实现一个优雅的下载队列。

准备好虚空造物,构筑你的第一条协程管道了吗?我们开始。

千曲 而后晓声,观千剑 而后识器。虐它千百遍 方能通晓其真意


什么是 Channel

Kotlin 协程的官方定义中:

Channel 是一个非阻塞的、可挂起的、先进先出(FIFO)的通信原语 。它提供 sendreceive 两个挂起函数,允许协程之间安全地传递数据。Channel 可以配置有限的容量,当缓冲区满时 send 挂起,当缓冲区空时 receive 挂起。

如果你熟悉 Java,可以将 Channel 理解为协程版的 BlockingQueue,但 sendreceive挂起 而非阻塞线程。

kotlin 复制代码
val channel = Channel<Int>()

// 生产者协程
launch {
    for (i in 1..5) {
        channel.send(i) // 发送数据,可能挂起
        println("发送:$i")
    }
    channel.close() // 关闭通道
}

// 消费者协程
launch {
    for (value in channel) { // 迭代接收,直到通道关闭
        delay(1000)
        println("接收:$value")
    }
}
flowchart LR subgraph Producer[生产者协程] P[send 发送] end subgraph Channel[Channel 管道] C[缓冲区] end subgraph Consumer[消费者协程] R[receive 接收] end P -->|数据| C C -->|数据| R style Producer fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style Channel fill:#fff3e0,stroke:#f57c00,stroke-width:2px style Consumer fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style P fill:#c8e6c9 style C fill:#ffb74d style R fill:#90caf9

核心特性

  1. 挂起而非阻塞send 在缓冲区满时挂起,receive 在缓冲区空时挂起,线程不阻塞。
  2. 容量策略:可配置缓冲区大小及溢出行为。
  3. 可关闭close() 后不再接受新数据,接收方可消费剩余数据。
  4. 支持迭代for (value in channel) 自动处理关闭。

Channel vs Flow vs SharedFlow:三国演义

许多开发者初学 Channel 时会困惑:它和 Flow、SharedFlow 到底有什么区别?为什么有了 Flow 还要 Channel?

对比维度 Channel Flow(冷) SharedFlow(热)
设计目的 协程间通信 异步数据流计算 多播事件/状态
通信模式 点对点(可多个接收者,但每条数据只被一个接收) 点对点(每个收集者独立流) 点对多(所有订阅者收到相同数据)
背压控制 缓冲区满时 send 挂起 拉取模型,消费者控制节奏 可配置缓冲与丢弃策略
历史重放 无(冷流) replay
关闭 需要显式 close() 无需,流自然结束 无需,但作用域取消时停止
典型场景 任务队列、Actor 模式 数据库查询、网络请求 UI 状态、事件总线
flowchart LR subgraph Channel[Channel] direction TB C1[send] --> CB[缓冲区] --> C2[receive] end subgraph Flow[Flow 冷流] direction TB F1[生产者] --> F2[操作符] --> F3[collect] end subgraph SharedFlow[SharedFlow 热流] direction TB S1[emit] --> SC[缓存/重放] --> S2[订阅者1] SC --> S3[订阅者2] end style Channel fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style Flow fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style SharedFlow fill:#ffcdd2,stroke:#b71c1c,stroke-width:2px style C1 fill:#c8e6c9 style CB fill:#ffb74d style C2 fill:#90caf9 style F1 fill:#90caf9 style F2 fill:#90caf9 style F3 fill:#90caf9 style S1 fill:#ef9a9a style SC fill:#ffb74d style S2 fill:#ef9a9a style S3 fill:#ef9a9a

简单选择法则

  • 需要协程间传递任务 ,且希望发送方可被背压挂起Channel
  • 需要声明式数据流转换 ,且每次收集都重新执行 → Flow
  • 需要多播事件或共享状态 ,且希望新订阅者收到历史 → SharedFlow / StateFlow

Channel 的四种容量策略

Channel 的构造函数接受一个 capacity 参数,它决定了缓冲区的行为和 send 的挂起时机。

kotlin 复制代码
val channel = Channel<Int>(capacity = Channel.RENDEZVOUS)
策略 容量 行为 适用场景
RENDEZVOUS(默认) 0 sendreceive 必须会合------发送方挂起直到有接收方,反之亦然 无缓冲,严格同步
BUFFERED 64(默认) 缓冲区满时 send 挂起 典型的生产-消费队列
CONFLATED 1(合并) 缓冲区只保留最新 值,新值覆盖旧值,send 永不挂起 只关心最新状态(如位置)
UNLIMITED 无限 send 永不挂起,需注意内存 生产者不受限,但消费者慢时可能 OOM
自定义 Int 指定值 缓冲区满时 send 挂起 精细控制内存与吞吐
flowchart TD subgraph Rendezvous[RENDEZVOUS 无缓冲] RS[send] -->|挂起| RW[等待 receive] RR[receive] -->|挂起| RW2[等待 send] RW -.->|会合| RW2 end subgraph Buffered[BUFFERED 有缓冲] BS[send] --> BB[缓冲区 64] BB --> BR[receive] BB -->|满时挂起| BS end subgraph Conflated[CONFLATED 合并] CS[send] --> CB[保留最新值] CS -->|覆盖旧值| CB CB --> CR[receive] end subgraph Unlimited[UNLIMITED 无限] US[send] --> UB[无限增长] UB --> UR[receive] end style Rendezvous fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style Buffered fill:#fff3e0,stroke:#f57c00,stroke-width:2px style Conflated fill:#ffcdd2,stroke:#b71c1c,stroke-width:2px style Unlimited fill:#e3f2fd,stroke:#1976d2,stroke-width:2px

示例:RENDEZVOUS 的会合行为

kotlin 复制代码
val channel = Channel<Int>(Channel.RENDEZVOUS)

launch {
    println("准备发送...")
    channel.send(1) // 挂起,直到有接收方
    println("发送完成")
}

launch {
    delay(1000)
    val value = channel.receive() // 此时发送方恢复
    println("接收:$value")
}
// 输出顺序:准备发送... → (1秒后)发送完成 → 接收:1

示例:CONFLATED 的合并行为

kotlin 复制代码
val channel = Channel<Int>(Channel.CONFLATED)

launch {
    repeat(5) {
        channel.send(it) // 永不挂起
        println("发送:$it")
    }
    channel.close()
}

launch {
    delay(500) // 模拟慢速消费者
    for (value in channel) {
        println("接收:$value")
    }
}
// 可能输出:发送:0,1,2,3,4 → 接收:0 → 接收:4
// 中间的值被覆盖丢失了

produce 构建器:简化生产者协程

Kotlin 提供了 produce 协程构建器,它创建一个新的协程,返回一个 ReceiveChannel。生产者协程内部可以使用 send 发射数据,当协程结束时自动关闭 Channel。

kotlin 复制代码
fun CoroutineScope.produceNumbers() = produce {
    for (i in 1..5) {
        delay(100)
        send(i)
    }
    // 协程结束,自动 close()
}

fun main() = runBlocking {
    val channel = produceNumbers()
    channel.consumeEach { value ->
        println(value)
    }
}

produce 的优势:

  • 自动管理 Channel 生命周期,无需手动 close()
  • 代码更紧凑,意图更清晰。
  • 返回的 ReceiveChannel 只能接收,类型安全。
sequenceDiagram participant Scope as 协程作用域 participant Producer as produce 协程 participant Channel as ReceiveChannel participant Consumer as 消费者协程 Scope->>Producer: produce { } Producer->>Channel: 创建 Channel Producer-->>Scope: 返回 ReceiveChannel loop 生产数据 Producer->>Producer: delay + send Producer->>Channel: 数据 end Producer->>Channel: 协程结束,自动 close() Consumer->>Channel: consumeEach Channel-->>Consumer: 数据流

Channel 的关闭与迭代

Channel 用完后需要关闭,否则接收方可能永远挂起。关闭后:

  • send 会抛出 ClosedSendChannelException
  • receive 在缓冲区空时会抛出 ClosedReceiveChannelException
  • 使用 for 循环迭代时会自动在关闭后退出。

安全迭代:consumeEach

consumeEach 是一个终端操作符,它迭代所有元素,并在完成后(或异常时)自动取消 Channel,确保资源释放。

kotlin 复制代码
channel.consumeEach { value ->
    println(value)
}
// 等价于手动 try-finally

关闭状态机

stateDiagram-v2 [*] --> Open : 创建 Open --> Sending : send Open --> Receiving : receive Open --> Closed : close() Closed --> Closed : 不能再 send Closed --> Draining : 缓冲区还有数据 Draining --> Closed : 缓冲区空 Closed --> [*] : 完全关闭

实战:用 Channel 实现下载队列

场景:用户可能同时点击多个文件下载,我们希望串行处理(一次只下载一个),并实时显示当前下载进度。Channel 是实现任务队列的完美选择。

kotlin 复制代码
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch

class DownloadViewModel : ViewModel() {

    sealed class DownloadEvent {
        data class Progress(val url: String, val percent: Int) : DownloadEvent()
        data class Complete(val url: String) : DownloadEvent()
    }

    // 任务队列:无缓冲,保证串行
    private val downloadQueue = Channel<String>(Channel.RENDEZVOUS)
    
    // 进度事件流(用 SharedFlow 多播给 UI)
    private val _progressFlow = MutableSharedFlow<DownloadEvent>()
    val progressFlow: SharedFlow<DownloadEvent> = _progressFlow.asSharedFlow()

    init {
        // 启动唯一的消费者协程,逐个处理下载任务
        viewModelScope.launch {
            for (url in downloadQueue) {
                downloadFile(url)
                _progressFlow.emit(DownloadEvent.Complete(url))
            }
        }
    }

    fun enqueueDownload(url: String) {
        viewModelScope.launch {
            downloadQueue.send(url) // 如果消费者正忙,这里会挂起排队
        }
    }

    private suspend fun downloadFile(url: String) {
        // 模拟下载,每 10% 报告一次进度
        for (progress in 0..100 step 10) {
            delay(500)
            _progressFlow.emit(DownloadEvent.Progress(url, progress))
        }
    }
}
flowchart LR subgraph UI[UI 交互] B1[点击下载 A] B2[点击下载 B] B3[点击下载 C] end subgraph Queue[Channel 队列] Q[send 排队] end subgraph Worker[消费者协程] W[串行处理] end subgraph Events[进度事件 SharedFlow] P[进度更新] end B1 --> Q B2 --> Q B3 --> Q Q --> W W --> P --> UI style UI fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style Queue fill:#fff3e0,stroke:#f57c00,stroke-width:2px style Worker fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style Events fill:#c8e6c9,stroke:#388e3c,stroke-width:2px style Q fill:#ffb74d style W fill:#a5d6a7 style P fill:#81c784

配合 Compose UI

kotlin 复制代码
@Composable
fun DownloadScreen(viewModel: DownloadViewModel = viewModel()) {
    val events by viewModel.progressFlow.collectAsState(initial = null)
    
    Column {
        Button(onClick = { viewModel.enqueueDownload("file1.zip") }) {
            Text("下载文件1")
        }
        Button(onClick = { viewModel.enqueueDownload("file2.zip") }) {
            Text("下载文件2")
        }
        
        when (val event = events) {
            is DownloadEvent.Progress -> Text("${event.url}: ${event.percent}%")
            is DownloadEvent.Complete -> Text("${event.url} 完成!")
            null -> Text("等待任务...")
        }
    }
}

设计要点

  • 使用 RENDEZVOUS 确保任务严格串行,消费者处理完一个才接收下一个。
  • 进度事件用 SharedFlow 多播,UI 可灵活订阅。
  • send 挂起自动形成等待队列,无需手动管理。

常见错误与避坑指南

错误 1:忘记关闭 Channel,导致接收方永久挂起

kotlin 复制代码
val channel = Channel<Int>()
launch {
    repeat(3) { channel.send(it) }
    // 忘记 close()
}
for (value in channel) { // 接收完 0,1,2 后永久挂起
    println(value)
}

正确 :发送完后 channel.close(),或用 produce 构建器自动关闭。

错误 2:在 CONFLATED Channel 上期望收到所有值

kotlin 复制代码
val channel = Channel<Int>(Channel.CONFLATED)
repeat(10) { channel.send(it) }
// 消费者只能收到第一个和最后一个,中间丢失

正确 :只关心最新状态时才用 CONFLATED,否则用 BUFFERED

错误 3:多个消费者同时从同一个 Channel 接收

kotlin 复制代码
val channel = Channel<Int>()
launch { for (v in channel) println("A: $v") }
launch { for (v in channel) println("B: $v") }
// 每个值只会被一个消费者收到,且分配是不确定的

正确 :Channel 是点对点 通信,如需多播,用 SharedFlowbroadcast Channel。

错误 4:在 UI 层直接使用 Channelreceive 导致挂起

kotlin 复制代码
// 错误:在 Composable 中直接调用挂起函数
@Composable
fun MyScreen() {
    val value = viewModel.channel.receive() // 编译错误
}

正确 :通过 SharedFlowStateFlow 暴露数据给 UI,Channel 仅作为内部通信。


最佳实践

  1. 优先使用 produce 构建器:自动管理生命周期,代码更安全。
  2. 根据业务选择容量策略 :任务队列用 RENDEZVOUSBUFFERED;状态更新用 CONFLATED
  3. 使用 consumeEach 安全迭代:自动处理关闭和异常。
  4. Channel 用于内部通信,对外暴露 Flow:保持架构清晰。
  5. 注意 Channel 的线程安全性:Channel 本身是线程安全的,但发送的值应为不可变或线程安全对象。
  6. 配合 actor 模式实现更复杂的状态管理(下一讲深入)。

总结与下回预告

恭喜,你已掌握 Channel 的虚空造物之术,炼虚境初阶修炼完成!

本讲核心收获

  • Channel 是协程间的挂起式通信管道,send/receive 不阻塞线程。
  • 四种容量策略:RENDEZVOUS(无缓冲)、BUFFERED(有缓冲)、CONFLATED(合并)、UNLIMITED(无限)。
  • produce 构建器简化生产者协程,自动管理 Channel 生命周期。
  • Channel 需显式关闭,consumeEach 是安全迭代的推荐方式。
  • Channel 用于点对点任务队列,多播场景应使用 SharedFlow

在下一讲 【炼虚境·中阶】 中,我们将深入 Channel 的高级应用:BroadcastChannel 的兴衰、actor 模式实现并发安全、以及 select 表达式的多路复用。届时你会明白:

  • 为什么 BroadcastChannel 被废弃,SharedFlow 如何取而代之?
  • 如何用 actor 构建无锁的并发状态?
  • select 如何同时等待多个 Channel 或挂起函数?

【当前境界修为面板】

当前境界 修炼技能 修炼进度 修炼心得
炼虚境 · 初阶 1、Channel 管道术 2、容量四策 3、produce 构建诀 当前进度35% 修为350/1000 下一突破[炼虚境 · 中阶] (需领悟:BroadcastChannel 迁移、actor 模式、select 多路复用) Channel是协程间的BlockingQueue,但它不阻塞线程。send满则挂起,receive空则等待。

【本讲思考题】

  1. 表象题:以下代码的输出是什么?

    kotlin 复制代码
    val channel = Channel<Int>(Channel.CONFLATED)
    channel.send(1)
    channel.send(2)
    println(channel.receive())
    println(channel.receive())
  2. 场景题:你需要在 ViewModel 中实现一个"消息中心",多个协程可以向它发送日志消息,一个专门的协程负责将日志批量写入文件(每次收集 10 条或每 5 秒写入一次)。应该使用什么 Channel 策略?写出核心结构。

  3. 原理题Channelsend 挂起时,协程是如何被"唤醒"的?请从 AbstractSendChannel 和协程调度器的角度简述其内部挂起与恢复机制。


道友,炼虚境的大门已敞开。掌握了 Channel 的高级应用,你的协程通信将更加灵动。炼虚境·中阶见。

欢迎一键四连关注 + 点赞 + 收藏 + 评论

相关推荐
小强闯江湖41 分钟前
ViewCompose:让原生 Android View 进入声明式时代
android·开源·kotlin
提线木偶41 分钟前
CORS 到底谁说了算?一份跨域配置的避坑指南
前端·后端
网安蟹佬霸2 小时前
区块链与智能合约安全实战:从Solidity审计到DeFi漏洞深度剖析
运维·前端·网络·安全·自动化·区块链·智能合约
念何架构之路2 小时前
Gin响应渲染
前端·javascript·gin
invicinble2 小时前
设计网站的底层思路(深刻版本)
前端
三8442 小时前
WordPress REST API 参数校验机制剖析:为什么 author__not_in 无法直接盲注?
服务器·前端·数据库
ITmaster07312 小时前
Vibe Coding 时代:Vue 消失了还是 React 太强?
前端·vue.js·react.js
WebInfra3 小时前
Rspack 2.2 发布:30+ 项性能优化,拥抱 Solid 2.0
前端·javascript·前端框架
额额额对了3 小时前
Linux 进程管理详解:从概念到实战
java·服务器·前端