让转换并发起来的两个核心手段:flatMapMerge(concurrency = N)和channelFlow { ... }

一、

flatMapMerge(concurrency = N):把"元素→子流"并发执行并合并输出

语义

  • 对上游每个元素 x,用变换函数生成一个 子 Flow:x -> Flow。
  • 最多同时收集 N 个子流 (每个子流在独立协程 中运行),它们的结果交错合并 到下游(不保序)。
  • 如果活跃子流已达 N,外层对后续元素的处理会挂起 ,直到有子流结束(这就是并发闸门)。
scss 复制代码
ids.asFlow()
  .flatMapMerge(concurrency = 8) { id ->
     flow { emit(api.fetch(id)) }     // 每个 id 一条子流
       .flowOn(Dispatchers.IO)        // 子流内部切到 IO
  }
  .collect { println(it) }            // 结果交错到达

背压/取消/错误

  • 背压 :当下游慢、合并缓冲满时,子流的 emit 会挂起 ;若外侧也满了,新元素进入会被阻塞

  • 取消 :下游取消 → 所有活跃子流一并取消

  • 错误 :任意一个子流抛异常 → 整个合并流失败、其它子流被取消(除非你在子流内部 try/catch 吃掉)。

何时用

  • "一批任务并发处理 + 合并结果",例如:

    • 并发请求/解析:ids -> fetch(id)。

    • 并发压缩/转码:files -> transcode(file).

    • 并发数据库查询/本地计算。

和同族的区别

  • flatMapConcat:顺序收集每个子流(不并发,保序)。

  • flatMapLatest:新元素到来时取消旧子流(只保"最新一条"的处理)。

常用范式

1) "并发 map" 的等价写法(最实用)

kotlin 复制代码
inline fun <T, R> Flow<T>.mapAsync(
  concurrency: Int,
  crossinline block: suspend (T) -> R
): Flow<R> =
  flatMapMerge(concurrency) { t ->
    flow { emit(block(t)) }
  }

// 用法
ids.asFlow().mapAsync(8) { id -> api.fetch(id) }

2) 并发但保序输出(简易有序版)

注意:保序通常意味着牺牲一部分吞吐。

scss 复制代码
data class Indexed<T>(val i: Int, val v: T)

val ordered: Flow<Result> =
  ids.withIndex().asFlow()
    .flatMapMerge(concurrency = 8) { (i, id) ->
      flow { emit(Indexed(i, api.fetch(id))) }
    }
    .toList()             // 若必须严格保序,需聚合后再排序(会失去流式)
    .sortedBy { it.i }
    .asFlow()
    .map { it.v }

真正"边并发边保序且保持流式"需要更复杂的"有序缓冲"实现(可自写 mapAsyncOrdered)。


二、

channelFlow { ... }:用Channel搭一个"并发生产 → 单流输出"的自定义源

语义

  • 是一个 Flow 构建器 ,内部有一个 ProducerScope(带 send/trySend),你可以在块内 launch 多个子协程并发 地往同一个 Channel 发送元素。
  • channelFlow 会把这些元素按到达顺序 送给下游;是冷流 (collect 才启动),并遵守结构化并发(collect 结束/取消,会取消它内部的所有子协程)。
  • 内部使用一个有界 Channel (实现通常相当于 Channel.BUFFERED),send 在满时会挂起(背压)。
kotlin 复制代码
fun subscribeMarkets(matchId: String): Flow<OddsTick> = channelFlow {
    val markets = listOf("1x2", "ahcp", "ou")
    markets.forEach { m ->
        launch(Dispatchers.IO) {
            provider.subscribe(matchId, m) { tick ->
                trySend(tick).isSuccess   // 不阻塞版本,失败可按需丢弃或重试
            }
        }
    }
    // 结束条件:由下游取消,或你自行 close
    // 不需要 awaitClose(callbackFlow 适配回调时才常用)
}

对比 callbackFlow :二者都基于 Channel。callbackFlow 面向"把单个回调源 适配成 Flow"(需要 awaitClose { ... } 解绑回调);channelFlow 更像"我来管多个并发生产者"。

背压/取消/错误

  • 背压:send 满时挂起;trySend 立即返回失败让你自定丢弃策略(如只保最新)。

  • 取消 :下游取消/完成 → channelFlow 作用域整体取消,子协程全停。

  • 错误 :块内任一子协程未捕获异常 → 整个 channelFlow 失败;若要"一个失败不影响其他",在子协程里 try/catch 或用 supervisorScope。

何时用

  • 一个元素需要扇出 为多条并发子任务/子订阅,再汇入同一条流(例如:一场比赛订阅多个盘口,多源合并)。
  • 需要把外界主动推送(WebSocket、系统回调、多路监听)汇聚成 Flow。
  • 想精细控制 "在块内自己起/停子协程" ,不适合用 flatMapMerge 时。

三、如何选:flatMapMerge vs channelFlow

场景 选哪一个 说明
"每个输入元素"对应"一条子流/一次异步任务",并发处理后合并结果 flatMapMerge(N) 写法简单,天然有并发闸门(concurrency)。
需要在一个构建器内起多个并发生产者 ,或把多路回调/订阅汇入 channelFlow 你手动 launch/send,最灵活。
需要保序输出 flatMapConcat 或自写有序缓冲 并发+保序要额外结构。
需要"最新优先,旧任务取消" flatMapLatest / mapLatest 不是合并,而是取消旧
需要适配单个回调 API(注册/反注册) callbackFlow 兄弟 API,常配 awaitClose{}。

四、和flowOn/buffer的放置策略

  • 子任务跑哪 :把重活写在 子流/子协程内部,然后对它们 flowOn(Dispatchers.IO/Default) 或 launch(Dispatchers.IO)。

  • 边界缓冲

    • flatMapMerge 之后可接 .buffer(k) 给合并输出再加一层队列,削峰。
    • channelFlow 外侧也可接 .buffer(k);内侧用 send/trySend 决定"阻塞还是丢弃"。
  • 只要最新:在 UI 侧接 conflate() 或 buffer(1, DROP_OLDEST),保证高频场景不压爆渲染。


五、组合范式(可直接套)

1) 并发网络 + 合并(限并发 8)

scss 复制代码
ids.asFlow()
  .mapAsync(concurrency = 8) { id -> api.fetch(id) } // = flatMapMerge + flow{emit}
  .buffer(256)
  .flowOn(Dispatchers.IO)
  .collect { render(it) }

2) 单输入扇出为多任务并发,再汇入(用channelFlow)

scss 复制代码
fun enrich(userId: String): Flow<UserFull> = channelFlow {
    val acc = AtomicReference(UserFull.empty(userId))

    fun emitIfReady() {
        acc.get().takeIf { it.allReady() }?.let { trySend(it) }
    }
    launch(Dispatchers.IO) {
        val p = repo.profile(userId)
        acc.updateAndGet { it.copy(profile = p) }
        emitIfReady()
    }
    launch(Dispatchers.IO) {
        val o = repo.orders(userId)
        acc.updateAndGet { it.copy(orders = o) }
        emitIfReady()
    }
    launch(Dispatchers.IO) {
        val r = repo.risk(userId)
        acc.updateAndGet { it.copy(risk = r) }
        emitIfReady()
    }
}
  .conflate()                 // 只要最新聚合结果
  .sample(100)                // 限刷新频率

3) 推送聚合(多盘口/多源 → 一条 UI 流)

scss 复制代码
fun oddsFlow(matchId: String): Flow<Odds> = channelFlow {
    listOf("1x2","ahcp","ou").forEach { m ->
        launch(Dispatchers.IO) {
            provider.subscribe(matchId, m) { tick ->
                // 背压下丢旧留新
                trySend(tick).isSuccess
            }
        }
    }
}
  .buffer(256)                // 外侧再加一段队列
  .conflate()                 // UI 只要最新
  .sample(200)                // 帧率对齐

六、易坑与规约

  1. 保序误用 flatMapMerge :它不保序;要保序用 flatMapConcat,或自建有序缓冲。
  2. channelFlow 内忘记处理失败 :任一子协程异常会整流失败;关键子任务用 try/catch 或 supervisorScope。
  3. 无限并发:flatMapMerge 大并发 + 每个子流还起子协程,容易把线程/连接打满;务必控好 concurrency 与 dispatcher。
  4. 放错 flowOn :谁想在哪个线程跑,就把它写在对应 flowOn 的上游。在 flow{} 中 withContext 违规。
  5. 背压策略混乱 :UI 只需"最新"时,用 conflate()/DROP_OLDEST;统计/审计流需"必达",就用 SUSPEND 并持久化,拆两条管道
  6. 在 channelFlow 里阻塞 send:若不想因为背压挂起生产者,请用 trySend 并结合外侧 buffer()/conflate() 设计丢弃策略。

结论

  • 批量并发、元素级并行:优先 flatMapMerge(concurrency = N)(语义简洁、闸门好用)。
  • 多源并发生产/汇聚、复杂扇出-汇入:用 channelFlow { ... }(你掌舵并发与发送时机)。
  • 配合 flowOn/buffer/conflate/sample/*Latest,就能把吞吐、实时性、稳定性三者调到业务需要的平衡点。
相关推荐
9号达人2 小时前
Java 14 新特性详解与实践
java·后端·面试
青鱼入云4 小时前
【面试场景题】支付&金融系统与普通业务系统的一些技术和架构上的区别
面试·金融·架构
掘金安东尼4 小时前
黑客劫持:周下载量超20+亿的NPM包被攻击
前端·javascript·面试
在未来等你9 小时前
Elasticsearch面试精讲 Day 17:查询性能调优实践
大数据·分布式·elasticsearch·搜索引擎·面试
围巾哥萧尘13 小时前
美式审美的商务头像照🧣
面试
不要再敲了18 小时前
JavaScript与jQuery:从入门到面试的完整指南
javascript·面试·jquery
月阳羊19 小时前
【硬件-笔试面试题-95】硬件/电子工程师,笔试面试题(知识点:RC电路中的时间常数)
java·经验分享·单片机·嵌入式硬件·面试
yinke小琪19 小时前
说说hashCode() 和 equals() 之间的关系
java·后端·面试
Aerfajj20 小时前
从零开始搭建一个新的项目,需要配置哪些东西
面试