第3章 Flow 创建与基础消费:flow 构建器、collect 操作符、默认背压行为

3.1 核心原理:冷流与挂起驱动的背压

Kotlin Flow 是一种冷数据流,即生产者(上游 Flow)只有在被终端操作符消费时才开始发射数据。其默认背压行为不依赖缓冲区或丢弃策略,而是通过协程挂起机制自然实现:collect 每处理一个值,上游 emit 必须等待该值被完全消费后才能发射下一个值。这种 “推拉结合” 的模式保证了数据不会在消费者未准备好时堆积,从而本质上避免了内存溢出或 CPU 空转。

关键理解:Flow 的默认背压不是“控制速率”,而是“等一等生产者”。每次 emit() 调用都会挂起当前协程,直到 collect 中的 lambda 执行完毕,再恢复发射下一个元素。这相当于生产者和消费者之间有一个隐式的一对一同步通道。

3.2 实现流程与 API 签名

创建并消费一个 Flow 通常分三步:

  1. 构建:使用 flow { ... } 构建器定义发射序列。
  2. 中间操作(可选):应用 map、filter 等操作符转换数据。
  3. 终端消费:调用 .collect { ... } 开始拉取数据。

关键 API 签名

API类型说明
fun flow(block: suspend FlowCollector.() -> Unit): Flow构建器在协程上下文中调用 emit() 发射数据
suspend fun Flow.collect(action: suspend (T) -> Unit)终端操作符消费每个元素,挂起直到所有元素被处理
suspend fun FlowCollector.emit(value: T)发射函数向上游 FlowCollector 发出值,挂起等待消费者处理

3.3 代码示例:基础消费与默认背压行为

以下代码演示了 flow { ... } 和 collect 的典型用法。注意 emit 与 collect 之间的挂起关系。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val flow = flow {
        println("Flow 开始发射")
        for (i in 1..3) {
            delay(100) // 模拟异步工作
            println("emit: $i")
            emit(i)
        }
        println("Flow 发射结束")
    }

    println("开始 collect")
    flow.collect { value ->
        delay(200) // 模拟较慢的消费者
        println("collect: $value")
    }
    println("collect 完成")
}

输出顺序验证背压行为:

开始 collect
Flow 开始发射
emit: 1
collect: 1
emit: 2
collect: 2
emit: 3
collect: 3
Flow 发射结束
collect 完成
分析:消费者 collect 处理每个值需要 200ms,而生产者每次发射前只等待 100ms。尽管如此,由于默认背压的挂起机制,生产者必须等待消费者完成 collect: 1 后才能发射第 2 个元素。整个流程被拉长到约 900ms,没有出现数据堆积或背压溢出。

3.4 默认背压行为结构图

下图展示了 flow 构建器与 collect 之间的交互过程,强调挂起点和背压传递关系。

Flow 基础消费与默认背压行为流程 Flow 基础消费与默认背压行为 flow { … } 发射侧协程 emit(value) 挂起,等待消费 collect { … } 消费侧协程 消费完毕 恢复上游 emit 挂起,传递背压 默认背压行为 每次 emit 后生产侧挂起,必须等 collect 处理完当前值才能继续发射 生产者 消费者

3.5 常见错误与避坑

  • 忽略挂起特性导致顺序假设错误:误以为 emit 和 collect 是异步并行的。实际上它们是严格串行的,emit 后的代码要等 collect 执行完毕才能继续。
  • 在 flow 构建器中直接使用阻塞 API:例如调用 Thread.sleep() 而非 delay(),会阻塞协程线程,破坏流程控制。所有耗时操作应使用挂起函数。
  • 忘记调用 collect 导致 flow 不执行:Flow 是冷流,若不调用终端操作符,构建器内的代码永不会执行。调试时常见此问题。
性能陷阱:默认背压虽然安全,但如果消费者处理极快(< 1μs)而生产者发射密集(如循环无延迟),会导致消费者每步等待时间极短,但整体仍存在协程上下文切换开销。对于海量数据的批处理场景,考虑引入 buffer() 操作符以合并发射批次,减少挂起次数。但本章默认行为下不应随意加 buffer 以免混淆基础概念。

3.6 调试与日志技巧

在生产代码中观察背压行为最简单的工具是在 emit 和 collect 前后添加协程上下文日志:

flow {
    println("[${coroutineContext}] 准备发射 $value")
    emit(value)
    println("[${coroutineContext}] 发射完成")
}.collect {
    println("[${coroutineContext}] 消费 $it")
    // 处理
}

利用 coroutineContext 打印的线程名称和 Job 状态,可以直观看到 emit 与 collect 在同一协程上下文中的交替执行顺序。

3.7 稳定性与默认背压的可靠性

由于默认背压不依赖缓冲区,不产生数据丢失或溢出,因此是构建可靠流式处理管道的基础。即使消费者偶尔慢速(如网络 I/O)、生产者突发高速,系统也不会崩溃,仅表现为总处理时间延长。这一特性使得 flow{…}.collect{…} 可安全用于关键路径,如传感器数据采集、文件逐行读取等场景。

特性默认背压(挂起式)缓冲区背压(如 buffer, conflate)
数据丢失风险无(一对一交付)有(丢弃策略)
内存占用低(单元素)高(缓冲区大小)
适用场景可靠处理、顺序依赖高吞吐、可容忍少量丢失

在进入更高级的背压控制操作符之前,深刻理解默认的挂起驱动背压是避免后续混淆的根基。