Flow执行模型:上下文保持与生产消费节奏

一条Flow链不仅描述数据如何转换,也隐含两组运行约束:每个操作符在哪个协程上下文中执行,以及生产速度超过消费速度时如何处理积压数据。

flowOn解决上游执行位置,bufferconflatecollectLatest解决生产消费节奏。它们都可能改变运行过程,却不应改变业务语义。上下文应服从任务类型,背压策略则必须服从每条数据是否允许跳过、旧处理是否已经失效。

内容摘要

  1. Flow遵循上下文保持原则,下游默认运行在收集协程的上下文中。
  2. flowOn只影响它前面的上游,不会把后面的collect一起切走。
  3. 多个flowOn可以把Flow链划分为不同执行区段。
  4. buffer保留数据并解耦生产消费,缓冲区满后上游仍会挂起。
  5. conflate跳过积压的中间值,collectLatest还会取消旧值的处理代码块。

一、上下文保持让下游位置可预测

scss 复制代码
viewModelScope.launch {
    // 下游默认继承启动收集的 viewModelScope 上下文
    repository.users()
        // 没有 flowOn 边界时,转换与 collect 使用同一收集上下文
        .map(::toUiModels)
        .collect(::render)
}

collect使用启动收集的协程上下文。这里由viewModelScope启动,通常处于Dispatchers.Main.immediate,因此collectrender在Main上下文中执行。

collect本身并不固定属于主线程:

scss 复制代码
scope.launch(Dispatchers.Default) {
    // collect 并不固定属于 Main,这里会在 Default 上下文消费数据
    flow.collect(::calculate)
}

此时下游就在Default上下文中。准确关系是:

sql 复制代码
收集协程的Context
→ 决定下游操作符和collect的默认执行上下文

普通flow {}还要求发射遵守上下文保持。下面的写法不应使用:

scss 复制代码
flow {
    withContext(Dispatchers.IO) {
        // 错误示例:普通 flow 不允许切换上下文后再 emit
        emit(api.loadData())
    }
}

withContext改变了执行emit的上下文,破坏普通Flow对发射上下文的一致约束。Flow上游切换应由flowOn表达。

本节小结

Flow不会把整条链固定到某个线程。收集协程决定下游上下文,普通Flow保持发射上下文一致,从而让调用方可以预测数据最终在哪里被消费。

二、flowOn把上游划分为执行区段

scss 复制代码
viewModelScope.launch {
    flow {
        // 数据源位于第一个 flowOn 的上游区段
        emit(api.loadData())
    }
        .map(::parseData)
        // 数据源和 parseData 在 IO 上下文执行
        .flowOn(Dispatchers.IO)
        .map(::calculateResult)
        // calculateResult 位于新的 Default 上游区段
        .flowOn(Dispatchers.Default)
        // collect 不受前方 flowOn 影响,仍使用收集协程上下文
        .collect(::render)
}

判断范围时,从每个flowOn向前看,直到遇到另一个flowOn边界:

rust 复制代码
api.loadData、parseData → Dispatchers.IO
calculateResult         → Dispatchers.Default
collect、render         → 收集协程Context

flowOn在需要时会在上下游之间引入协程和通道边界,让不同区段在各自上下文中协作。它影响的是前面的上游,不会逆向改变后面的下游。

Dispatcher应匹配任务性质

css 复制代码
阻塞式网络、文件、数据库调用 → IO
CPU密集解析、排序、计算      → Default
UI渲染                     → Main

调度器不是越多越好。频繁切分区段会增加调度和数据传递成本,也会让链路更难阅读。若数据源本身已经保证在合适上下文执行,调用层无需重复添加flowOn

flowOn接收的是CoroutineContext,因此也可以附加调试名称:

less 复制代码
.flowOn(
    // flowOn 可以组合 Dispatcher 与调试名称等 Context 元素
    Dispatchers.IO + CoroutineName("LoadUsers")
)

withContextflowOn的职责边界

css 复制代码
普通挂起函数中的局部步骤 → withContext
Flow前方的一段上游链路  → flowOn

二者都可能改变Dispatcher,但表达的结构不同。withContext执行当前协程中的顺序代码块,flowOn为Flow操作符链建立上游上下文边界。

本节小结

flowOn只改变前方上游,并可通过多个边界形成执行区段。区段划分应反映IO、计算和UI任务的真实性质,不能把调度器当作性能装饰。

三、默认Flow如何协调生产与消费

scss 复制代码
flow {
    repeat(3) { value ->
        // 默认情况下,emit 会等待下游处理当前值后再继续
        emit(value)
    }
}.collect { value ->
    // 下游耗时会直接约束上游的生产速度
    process(value)
}

默认情况下,emit需要等待下游完成当前值的处理后才能继续,因此执行节奏近似:

复制代码
生产1 → 处理1 → 生产2 → 处理2 → 生产3 → 处理3

这种顺序传播提供天然背压:下游变慢,上游也会被约束,不会无限制地产生待处理数据。但它也意味着生产和消费耗时直接相加,无法重叠执行。

加入buffer后,上游和下游可以在不同协程中重叠运行:

scss 复制代码
records()
    // 有限缓冲让生产与消费重叠;缓冲满后上游仍会挂起
    .buffer(capacity = 64)
    // 每条记录都保留并最终交给持久化逻辑
    .collect(::persist)
复制代码
上游继续生产 → 数据进入缓冲区
下游同时消费 → 从缓冲区取数据
缓冲区已满   → 上游再次挂起

buffer原则上保留每条数据,但它不是无限仓库。容量会影响内存占用、生产阻塞时机和突发流量承受能力。

本节小结

默认Flow通过挂起上游形成背压,buffer通过有限缓存解耦生产和消费。数据必须完整处理时,可以调整吞吐和容量,但不能用丢值换速度。

四、只保留最新值有两种不同语义

conflate跳过等待中的中间值

scss 复制代码
downloadProgress()
    // 下游忙碌时只保留最新待处理进度,不取消当前渲染
    .conflate()
    .collect(::renderProgress)

当下游正在处理旧值时,新值持续到达,conflate只保留最新的待处理值:

css 复制代码
正在处理值A
值B、C、D陆续到达
→ A继续处理
→ B、C被替换
→ A完成后处理D

它适合进度、坐标、传感器快照等只关心近期状态的数据。交易记录、日志和操作命令等不可丢失数据不适合合并。

collectLatest取消旧处理

scss 复制代码
queryFlow.collectLatest { query ->
    // 新关键词到达时,旧关键词对应的代码块会收到取消信号
    val result = api.search(query)

    // 只有仍然有效的最新请求结果才继续渲染
    render(result)
}

新值到来时,旧值对应的收集代码块会被取消:

css 复制代码
开始处理A
值B到达
→ 取消A的处理
→ 开始处理B

这适合搜索联想、页面切换后的内容加载等"新输入使旧任务失效"的场景。

取消仍然是协作式的。网络挂起函数、delay等可取消挂起点可以及时响应;没有挂起点的长计算需要调用ensureActive()、检查isActive或适当yield()。否则旧计算可能在收到取消信号后仍继续占用线程。

三种策略的业务判断

业务要求 操作符 运行结果
每条数据都必须处理 buffer 暂存并完整消费
只需要较新的状态 conflate 当前处理完成,跳过积压中间值
新值使旧处理失效 collectLatest 取消旧处理,立即处理新值

本节小结

conflate丢弃等待中的旧值,不取消正在执行的处理;collectLatest连旧处理也会取消。两者的差异是业务有效期,不只是性能表现。

结语:执行优化不能改变数据含义

Flow运行模型可以分成两个坐标:

arduino 复制代码
在哪里执行
→ 收集Context + flowOn区段

数据来得太快怎么办
→ buffer / conflate / collectLatest

上下文选择决定资源匹配,节奏策略决定数据完整性和处理有效期。任何优化都应先证明不会改变业务结果,再讨论线程利用率、响应速度和内存容量。

参考资料

相关推荐
WAsbry1 小时前
协程任务的组织方式:launch、async、withContext与结构化并发
android
WAsbry1 小时前
本文区分原子变量、复合操作、并发数量与状态所有权四类问题,比较Atomic、Mutex、Semaphore和状态封闭的适用边界,并结合计数、缓存和限流场景,建立
android
2501_915909061 小时前
iOS test 测试怎么做?功能、性能、兼容、稳定与安全五类测试指南
android·ios·小程序·https·uni-app·iphone·webview
赵广陆2 小时前
企业实战:Markdown图片检索
android·java·开发语言
alexhilton3 小时前
千万别误用Android Skills
android·kotlin·android jetpack
淡淡的香烟4 小时前
Android视频直播播放器简单封装
android·物联网·音视频
爱笑鱼4 小时前
Android 系统启动机制导读:从 init 到 system_server,谁创建谁?
android
爱笑鱼4 小时前
Android 系统启动机制(一):init 到底是什么?为什么它不是普通 Native Service?
android
必须会一定会4 小时前
GLM-5.3 迁移实战:强制思考、1M 上下文与官方编程基准
android·开发语言·kotlin