Flow中的背压与并发

一、Flow 的背压(Backpressure)

1)默认策略:

挂起传播(suspend)

  • Flow 是冷流 ,上游 emit、下游 collect 默认是顺序串行

  • 当下游慢时:上游的 emit() 会挂起等待下游消费完,再继续发下一条------这就是 Flow 天然的背压。

2)改变背压行为的常用算子

算子 行为 典型用途
buffer(capacity) 在上下游之间放缓冲区;满了再挂起 "平滑"短时抖动;IO 与 UI 解耦
conflate() 只保留最新,丢弃中间值 UI 连续进度/位置,只关心最新
collectLatest { ... } 新值来时取消旧值的处理 搜索建议、图片解码、渲染任务
debounce(t) 静默期后发最后一个 文本输入防抖
sample(t) 每 t 时间取一个最新值 高频来源的节流
zip vs combine zip 配对等待;combine 来一个就合并最新 双源对齐 vs 实时混合

小结:不丢数据 用 buffer;要最新 用 conflate/collectLatest/sample/debounce;严格一一配对用 zip。

3)Hot Flow 的背压特性

  • StateFlow :天然合流/覆盖(只保留最新),不会"背压阻塞"上游。适合状态。

  • SharedFlow:可配置缓冲:

    • MutableSharedFlow(replay, extraBufferCapacity, onBufferOverflow = SUSPEND|DROP_OLDEST|DROP_LATEST)

    • 用于多订阅者广播时的背压策略选择。

4)flowOn与背压

  • flowOn(Dispatcher) 把其上的上游 移到指定线程,还会在边界放缓冲通道,用以解耦上下游速率,避免主线程被上游阻塞。
  • 若需要更强控制,可再配合 buffer()。

二、Flow 的并发(Concurrency)

1)Flow 的默认执行模型

  • 顺序执行:每个算子(map/filter/transform)在同一协程里一条一条处理。

  • 这意味着:map { withContext(Dispatchers.Default){...} } 不会并行,只是换了线程。

2)让转换并发起来的两个核心手段

A.flatMapMerge(concurrency = N)

把"单值→子流"的转换并并发运行 N 份 ,最终合并成一个流:

scss 复制代码
flowOf(url1, url2, url3, url4)
  .flatMapMerge(concurrency = 4) { url ->
    flow { emit(downloadAndDecode(url)) } // 每个子流可在 IO/Default 上工作
  }
  .flowOn(Dispatchers.IO)   // 上游下载在 IO
  .onEach { render(it) }    // 下游在调用处上下文(通常 Main)
  .launchIn(lifecycleScope)

B.channelFlow { ... }

在构建器里可以并发启动多个子协程,对同一 send() 通道推数据(安全、有序):

scss 复制代码
channelFlow {
  val urls = listOf(url1, url2, url3, url4)
  val sem = Semaphore(permits = 4)            // 自控并发度
  urls.forEach { url ->
    launch(Dispatchers.IO) {
      sem.withPermit {
        val bmp = downloadAndDecode(url)
        send(bmp)                              // 推给下游
      }
    }
  }
}.onEach { render(it) }.launchIn(lifecycleScope)

对比:flatMapMerge 简洁、足够常用;channelFlow 更灵活(并发结构、取消、超时、择优返回等)。

3)其它并发相关算子

  • mapLatest {} :新值来就取消旧任务(常用于 UI 渲染、搜索)。
  • merge(flowA, flowB, ...) :把多个流并发合并。
  • combine(flowA, flowB) :任何一端有新值就合并最新快照(不是配对)。

三、典型落地范式(可直接套用)

场景 1:IO → 解码 → UI 渲染(上下游解耦 + 背压可控)

scss 复制代码
imageIdFlow
  .map { id -> loadBytes(id) }                 // IO
  .flowOn(Dispatchers.IO)
  .buffer(capacity = 8)                        // 抗抖动,避免 UI 抽风卡顿
  .mapLatest { bytes -> decodeBitmap(bytes) }  // 新图来就取消旧解码
  .flowOn(Dispatchers.Default)                 // 解码放 CPU 线程池
  .onEach { bmp -> imageView.setImageBitmap(bmp) } // 主线程渲染
  .launchIn(lifecycleScope)
  • 背压策略:缓冲 8 张,解码用 mapLatest 丢弃过时任务,保证 UI 只渲染最新。

场景 2:列表批量任务并发(受限 N 路)

scss 复制代码
flow { mediaIds.forEach { emit(it) } }
  .flatMapMerge(concurrency = 4) { id ->
    flow {
      val meta = queryMediaStore(id)          // IO
      val thumb = decodeThumb(meta)           // CPU
      emit(thumb)
    }.flowOn(Dispatchers.IO)
  }
  .onEach { submitThumb(it) }
  .launchIn(lifecycleScope)
  • flatMapMerge(4) 控制并发;每个子流内再 flowOn(IO),CPU/IO 可再细分。

场景 3:搜索输入(防抖 + 取消旧请求)

scss 复制代码
textChangesFlow
  .debounce(300)
  .distinctUntilChanged()
  .mapLatest { q -> api.search(q) }           // 新词来了取消上一个网络请求
  .catch { emit(emptyList()) }
  .onEach(::renderResults)
  .launchIn(lifecycleScope)

场景 4:全局状态(只要最新,不做背压)

kotlin 复制代码
val _state = MutableStateFlow(UiState())
val state: StateFlow<UiState> = _state
// 频繁 setValue 也不会"压爆",下游永远拿到**最新** UiState

场景 5:广播事件(可配置溢出策略)

ini 复制代码
val events = MutableSharedFlow<Event>(
  replay = 0,
  extraBufferCapacity = 64,
  onBufferOverflow = BufferOverflow.DROP_OLDEST // 或 SUSPEND / DROP_LATEST
)

四、调度与上下游("上游/下游"分界)

  • 同协程、顺序执行是默认:一个操作卡住,后面都等。

  • flowOn(X) :把其上的链路切到调度器 X,并在此处放缓冲;其下游仍在调用方上下文(比如 Main)。

  • buffer() :可以在任意位置放;常见组合是 flowOn(IO).buffer().mapLatest{...}.flowOn(Default)。

口诀:先 flowOn 再 buffer 再"重活" ;UI 渲染在 Main,重活各归其位。


五、错误处理与取消

  • collectLatest/mapLatest 是通过取消旧协程来"丢弃"旧任务,注意释放资源(try {..} finally {..})。
  • catch {} 只能捕获其上游异常;要捕获下游请包在 runCatching {} 或在外层 try。
  • 对于 channelFlow/多并发子协程,建议用 SupervisorScope 防止一条失败把全局取消。

六、常见误区(避坑)

  1. "withContext 就并行了"

    不是。并发要用 flatMapMerge / merge / channelFlow。

  2. 在 UI 上游做重活

    记得 flowOn(IO/Default),并留出 buffer。

  3. 到处用无限 buffer

    易 OOM。容量要结合业务峰值测出来,或改用 conflate / sample。

  4. 把事件当状态用

    一次性事件用 SharedFlow;状态用 StateFlow(天然合并最新)。

  5. 热流重复做重活

    多个收集者共享上游:用 shareIn(stateIn),并合理 replay/WhileSubscribed。


七、迷你基准模板(压测背压与并发)

scss 复制代码
val t0 = SystemClock.elapsedRealtime()

(1..1000).asFlow()
  .onEach { delay(1) }                 // 模拟上游 1ms/条
  .buffer(64)                           // 改改容量看总耗时与内存
  .flatMapMerge(concurrency = 8) { i ->
    flow {
      // 模拟 CPU 重任务 2ms
      withContext(Dispatchers.Default) { busyLoop(2) }
      emit(i)
    }
  }
  .onCompletion { println("cost=${SystemClock.elapsedRealtime()-t0}ms") }
  .launchIn(GlobalScope)  // 仅演示,实际请用生命周期域

八、快速选型清单

  • 我不想丢数据:buffer(n) → 不够再扩 → 必要时 backpressure(挂起)
  • 我只要最新:conflate() / collectLatest {} / sample() / StateFlow
  • 我要 N 路并发:flatMapMerge(concurrency=N) / channelFlow + Semaphore
  • 多来源合并:merge()(并发) / combine()(最新) / zip()(配对)
  • 线程/调度:flowOn(IO/Default) + 必要 buffer(),UI 渲染在 Main
相关推荐
洋楼街的奇妙圆子2 小时前
学术论文检索聚合 MCP 服务
面试·mcp
muchan922 小时前
为什么“它”在业务逻辑上是最简单的?
前端·后端·面试
不要再敲了12 小时前
JDBC从入门到面试:全面掌握Java数据库连接技术
java·数据库·面试
Nan_Shu_61413 小时前
Web前端面试题(1)
前端·面试·职场和发展
围巾哥萧尘14 小时前
开发一个AI婚礼照片应用时,如何编写和调整提示词🧣
面试
大前端helloworld14 小时前
前端梳理体系从常问问题去完善-基础篇(html,css,js,ts)
前端·javascript·面试
围巾哥萧尘16 小时前
大型语言模型语境学习中的演示位置偏置(DPP Bias)研究🧣
面试
getdu17 小时前
Redis面试相关
数据库·redis·面试